Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.46.0-wmf.23
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
Mécanique, enseignée via l'Histoire des Sciences/Mouvement de Kepler
0
9581
763127
759204
2026-04-07T15:35:16Z
Peyraut
123445
/* L'hodographe est un cercle ; donc la trajectoire est une ellipse */ pour que ce soit compréhensible j'ai modifié quelques expressions mais je me suis arrêté au vecteur excentricité
763127
wikitext
text/x-wiki
<noinclude>{{Mécanique, enseignée via l'Histoire des Sciences}}</noinclude>
Il s'agit du mouvement d'un point dans un champ central '''F'''('''OM''') = - GMm. '''OM'''/OM³, dit Newtonien.
Kepler en a énoncé les 3 lois principales :
*La planète P a pour trajectoire une ellipse dont le soleil O est un foyer.
*Le rayon vecteur '''OP''' balaye des surfaces égales dans des temps égaux.
*Le carré de la période T du mouvement est comme le cube du grand axe, 2a, de l'ellipse.
La démonstration de ces faits revient à Newton (1684).
L'article mouvement keplerien de la Wiki a été beaucoup modifié.
Nous en rapatrions l'essentiel.
== Le mouvement est central ==
les conséquences immédiates sont :
* Le moment cinétique '''L''' est une constante '''Lo'''.(On pose '''L''' = m.'''C''')
* Donc la trajectoire est plane, perpendiculaire en O à L<sub>0</sub>
* Dans ce plan , le mouvement tourne autour de O ('''toujours dans le même sens''', choisi comme positif).
* La loi des aires de Kepler est satisfaite : dS/dt = C/2 = 1/2 r².d<math>\theta</math>/dt.
* Comme C est non nul, thêta est une échelle de temps (non linéaire) mais souvent utilisée(cf Note).
* L'hodographe et la trajectoire sont en '''correspondance directe''' : l'un donne l'autre. L'espace des phases sera donc bien R^2 x R^2 , mais de manière très simplifiée.
Note-annexe : historiquement, Ptolémée a utilisé theta' = MF'O = ~ t (+ O(t^3)), car cela suffisait pour les observations de l'époque : cela s'appelle la théorie de l'équant, elle sera vue en exercice.
Note 2 : on a excepté le cas L=0 comme physiquement irréalisable : on doit toujours pouvoir s'y ramener à la limite, et c'est un joli-exercice.
== L'hodographe est un cercle ; donc la trajectoire est une ellipse ==
==='''l'hodographe est un cercle :'''===
Poser p = C<sub>0</sub>²/GM (on verra que c'est la longueur du semi-latus-rectum (on dit aussi "paramètre" de l'ellipse), et V<sub>0</sub> = C<sub>0</sub>/p (qui est donc une vitesse, par ailleurs pseudo-scalaire). Alors, on trouve :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{V} = V_0 \times\vec{n}+ \vec{V_1} </math>
|}
|
| |
|}</div>
multiplier par vecteur(k).wedge et diviser par Vo ; on obtient :
=== la trajectoire est l'ellipse : ===
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e}\cdot \vec{r} = r - p </math>
|}
|
| |
|}</div>
===Démonstration :===
prendre comme échelle de temps theta(t) ; le Principe Fondamental de la Dynamique de Translation (PFDT) donne :
<div style="text-align: center;">
<math> \frac{d\vec{V}} {d\theta} = - V_0 \cdot \vec{u} </math>.</div>
donc, par intégration sur la variable theta avec le vecteur unitaire <math>\vec{n}</math> perpendiculaire à <math>\vec{u}</math> :
<div style="text-align: center;"><math>\vec{V} = V_0 \times \vec{n} + \vec{cste}</math>.</div>
soit :
<div style="text-align: center;"><math> \vec{V} = V_0 \times \vec{n} + \vec{V_1}</math>.</div>
Il y a évidemment beaucoup de manière de retrouver le vecteur constant "cste = V1" , en prenant deux valeurs de la vitesse remarquables ; par exemple, la vitesse à l'apogée et au périgée donnent: V(A) = V<sub>0</sub> + V<sub>1</sub> et V(A') = V<sub>0</sub> - V<sub>1</sub> avec V<sub>1</sub> = e×V<sub>0</sub>.
'''''nota bene''''' :''Et Voilà ! C'est fini'' ! L'hodographe est bien un cercle ( de rayon Vo = Co/p) ! La trajectoire sera donc FERMEE ! On obtient donc cette caractéristique FONDAMENTALE du mouvement dès le début du raisonnement. Cette simple remarque a été faite en 1713, mais est passée relativement inaperçue. Il en est résulté des dizaines de re-découvertes ! Jusqu'en 2000, on peut voir des articles ( cf par exemple Butikov, etc.)signalant cette "trouvaille". On peut s'amuser à exploiter cet hodographe, sans doute comme l'a fait Hooke ( tentative dite des elliptoïdes ; rappelons que Hooke n'avait pas grande culture mathématique, mais il avait compris le principe de l'hodographe, puisque c'est cette méthode de l'hodographe qu'il utilise pour l'ellipse dite de Hooke).
==== '''Vecteur excentricité''', {{MathText|\vec{e_o}|eo}}, constant ====
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = \vec{u} + \vec{V/V_0} \wedge \vec {k} </math>
|}
|
| |
|}</div>
C'est l'extraordinaire intégrale première de Hermann(1713)- retrouvée par Laplace-Runge-Lenz,etc.! Il en sera question plus tard.
La démonstration est immédiate : multiplier l'équation de l'hodographe par vec(r)/Vo.wedge, et la réécrire .
===='''donc la trajectoire est une ellipse :'''====
Car en multipliant scalairement le vecteur-excentricité <math>\vec{e}</math> par le rayon-vecteur, on obtient :
<math>e \cdot r cos\theta = r - p</math> , soit :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> r = \frac{p}{1 - e \cos \theta} </math>
|}
|
| |
|}</div>
Ce qui est l'équation polaire d'une ellipse d'excentricité e , et de paramètre p , le vecteur-excentricité pointant vers l'apogée. La valeur de p ( demi-latus rectum := b^2/a := a(1-e^2)) est :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> p = \frac{C_o^2}{(GM)}</math>
|}
|
| |
|}</div>
Évidemment, on peut prendre la convention, '''origine au périgée''' ; soit <math> r = \frac{p}{1 + e \cos \theta}</math>,
==='''La conservation de l'énergie''' ===
si l'on introduit l'énergie potentielle -GMm/r , elle conduit à :
1/2 V² - (GM)/r = Eo/m = cste , d'où
<div style="text-align: center;">'''Eo négative == - GMm/2a.'''</div>
'''Exercice''' : montrer que 2a est le grand-axe de l'ellipse.
Donc dans le plan de la trajectoire, les deux quantités physiques '''Lo''' et Eo déterminent la forme de l'ellipse. Bien sûr '''OMo''' et '''Vo''' aussi.
==='''moyens mnémotechniques''' par @d ===
il importe, dans les exercices, de ne pas toujours tout redémontrer, et de savoir retenir les formules encadrées : la méthode d'A.D., dite des d@hus, sert en ce genre de situation :
les seuls paramètres sont cinématiques : GM (constante de Gauss) , Eo/m (énergie massique), et Co (constante des aires).
Donc, un de trop !
'''MAIS''' il suffit de retenir
*p = @d[GM, Co] ( et pas de Eo) ; et de retrouver la constante par le cas particulier du cercle (donc constante = 1)
*2a= @d[GM, Eo/m] ( et pas de Co) ; et de retrouver la constante par le cas particulier du cercle (donc constante = -1)
=== remarque de Hooke-Hamilton ===
Signalons à titre de curiosité ce raisonnement de Hooke, qui a peut-être des résurgences dans la pensée de Allais (Nobel économie quand même !) :
Si l'on considère que le mouvement est plan central, de centre O , pourquoi ne pas dire que la force est centrale et proportionnelle à l'angle balayé par unité de temps, soit <math>\dot{\theta}</math> , alors on retrouve tous les résultats antérieurs. Il est fort possible que ce soit par cette méthode que Hooke ait essayé de retrouver "la fameuse loi en 1/r²" , en appliquant sa méthode du second ordre : se donner la position initiale, puis la position voisine. Alors appliquer la loi et trouver la position ultérieure. Itérer. Il trouva par cette méthode des "elliptoides", ce que méprisa Newton. Plus fin, mais quel mérite en 1820? , Hamilton tirera de cette loi le fait que l'hodographe est un cercle, et tout le reste s'ensuit comme on l'a vu.
Ainsi les lois de Newton seraient simplement liées à un <math>\dot{\theta}</math>. Cette méthode serait plus "économique". Par contre, elle induirait peut-être un malaise, si on l'interprète à la manière Allais, car alors l'interposition de la Lune entre Soleil et Terre pourrait modifier l'angle sous lequel le Soleil serait vu de la Terre, et ainsi modifier "G" : une telle manière de faire serait alors en contradiction avec l'astronomie des trois corps. Il faudrait aussi retrouver la gravimétrie et les "théorèmes remarquables de newton-gauss". Dans cette problématique, on serait alors entraînés fort loin...Cela est bien curieux et ne vaut que pour l'anecdote : il est sain d'avoir toujours des visions différentes ( mais si elles débouchent...sur quelque chose de tangible).
== Mouvement sur la trajectoire ==
* La loi des aires donne S/T = Pi.a.b/T = Co/2 , ce qui donne :
{{exemple||loi de Kepler(1628)|<math>\omega^2 \cdot a^3 = (GM) </math>}}
* Partant du périhélie, et en introduisant l'angle dit [[anomalie excentrique]] E(t)(cf dessin), géométriquement :
<math>tan \theta/2 = tan E/2 \cdot \sqrt \frac{1+e}{1-e}</math>
<math> r = a (1- e \cdot \cos E)</math>;
On calcule géométriquement l'aire balayée depuis le passage au péricentre :
par affinité , S(t) = (b/a)[a²E/2 -ac. sinE /2] = ba(E-e.sinE)/2
Il s'ensuit :
{{exemple||Équation du temps de Kepler|<math>\omega t = E - e \cdot \sin E </math>}}
La fonction réciproque donne E(t), et de là '''OM'''(t).
----
===Fin du Cours===
Il est évident que l'on a cherché ici la compaction maximum du cours.Des dizaines d'ouvrages reprennent ce problème.
Pour nous, 2 ressortent du lot : Chandrasekhar si on aime la géométrie . Tisserand ou Winter si on veut plus exhaustif.Quelques exercices classiques suivent, pour "se faire la main".
----
==Exercices ==
Il y a des dizaines d'exercices sur ce sujet, évidemment très important; soit de satellites artificiels, soit d'astronomie. Nous "essaierons" de les classer.
=== satellites de la Terre ===
'''exMersenne-Descartes-Laplace :'''
Mersenne posa à Descartes la question suivante : si on tire un boulet verticalement, est-il possible que le boulet ne redescende pas?
Soit h = Vo²/2g . Montrer que l'altitude H atteinte est :
1/H = 1/h-1/R . que se passe-t-il pour h > R .
Que penser du cas Vo<c et c²< 2gR (Laplace vers 1800).
----
'''Système d'unités :''' pour la Terre , nous éviterons GM remplacé par gR² avec profit. Du fait de La loi de Galilée, la masse du satellite m n'intervient jamais. On se retrouve donc avec un système d'unités adapté ( un d@hu) tronqué à la cinématique.
*R étant l'unité de longueur, on prendra 2π.R = 40 000 km.
*On conviendra de prendre g = 9,80 m/s².
*La pulsation unitaire sera donc <math>w = \sqrt{\frac{g}{R}}</math>, dite pulsation de Schuler. Il lui correspond une '''période''' <math>T(R)= 2 \pi \sqrt{\frac{R}{g}}</math>, dite période basse altitude (84,4 min).
*La vitesse unitaire est <math>Vo = wR = \sqrt{gR}</math>= 1re vitesse cosmique = 8.2 km/s (vitesse d'un satellite basse altitude).
*L'énergie massique du satellite est donc -1/2 .gR
*Le pivotement sidéral de la Terre est 24h * (365.25/366.25) = 86164 s =17.0 To.En un jour les astronautes voient donc environ 18 fois le Soleil se lever.
En pratique, les satellites d'observation , type Spot orbitent à ~ 800 km d'altitude.
Reprendre le système d'unités de ces satellites.
----
'''Légère erreur de trajectoire :'''
Au lieu de la bonne vitesse Vo de Spot, on donne une vitesse de bonne direction (i.e perpendiculaire au rayon) mais trop forte : V1 = Vo(1+eps). Trouver la trajectoire et la période.
- - - - -
'''Fenêtre de tir :'''
m ex que le précédent mais la bonne vitesse Vo est mal orientée dans le plan d'un angle A , petit. Trouver le périgée.
- - - - -
'''Erreur radiale :'''
m ex que le précédent, mais il y a en sus de Vo , une erreur de vitesse radiale Vo.eps.
----
'''Lâcher-Chute libre :'''
On n' a pas attendu Newton (le 24 Nov 1679) pour réfléchir à la déviation vers l'Est (ou l'ouest!) d'une pierre lâchée de l'équateur; c'était la dispute favorite des Coperniciens et anti-Coperniciens. La vitesse due au pivotement est à l'équateur de 40 000 km/86164 s soit 464 m/s . Selon les anti-Coperniciens, une chute de 5m (environ 1s) eût placé le mobile vers l'Ouest de 464 m ! Galilée (mais il avait tort) disait que le corps tomberait toujours à la verticale. Koyré catalogue les différents types de solutions (chute des graves et mouvement de la Terre): l'imagination au pouvoir ! mais c'est Newton qui donna la solution.
Soit h << R , retrouver le résultat de Newton.
Si h est assez grand, la déviation vers l'est sera si grande que la pierre sera satellite.
Si h = altitude géostationnaire = H , la pierre ne tombe plus !
Si h est encore plus grand , la pierre est à son périgée : elle remonte, périodiquement.
Si h > (R+H) .2^(1/3) - R , qu'arrive-t-il ?
----
'''Balistique :''' voir la WP ( [[ellipse de sûreté]] )
revoir la leçon sur la chute libre avec violence (avec vitesse initiale dit-on aujourd'hui).
Dès que l'on veut une certaine précision (théorique) , il faut tenir compte de ce que la Terre est sphérique et donc prendre comme trajectoire de l'obus une ellipse lancé d'une base B avec une vitesse Vo faisant l'angle A avec la verticale. Soit u = Vo/sqrt(gR).
1/. Relation u et A pour que l'obus tombe à l'antipode.
2/. Déterminer la portée 2R.Beta , via tan B = f(u, tan A).
3/. Pour B donné, combien y a-t-il de trajectoires possibles ? et quelle est la portée maximale.
(Indication : soit H le point d'altitude maximale (pour A=0 !). La trajectoire a pour deuxième foyer un point situé sur le cercle [centre B ; rayon BH]).
----
=== Corrigé des exercices ===
'''Mersenne-Descartes :'''
Appliquer le théorème de l’Énergie cinétique :
-gR²/r +1/2 V² = constante , ce qui conduit au résultat.
Descartes évidemment ne savait rien de tout cela ; mais il se doutait "intuitivement" que si g(z) décroissait alors il y aurait possiblement une "vitesse de libération".
De même , Laplace , très heuristiquement , remarqua que si aucun corps ne pouvait dépasser la vitesse-limite c , alors si c² < 2gR , l'astre serait un trou noir !
Enfin, l'expérience a été tentée ( plus pour tester la relativité galiléenne et/ou la déviation vers l'Est(cf exo plus loin)): bien sûr on n'a jamais retrouvé le boulet! )
----
'''Système d'unités Spot :'''
Ro = 40 000/2Pi +800 = 7166 km.
To via Kepler est : 84.4 (7166/6366)^3/2 = 100 min.
Tout le reste s'en déduit (attention , c'est la pulsation qui a été choisie unitaire).
----
'''Légère erreur de trajectoire :'''
Si eps = sqrt(2) -1 , la trajectoire est parabolique et le satellite part à l'infini.
Sinon , Mo est le périgée: a-c = Ro. D'autre part, E1/m = 1/2 V1² - gR²/Ro ; donc on obtient le grand axe , puis l'apogée en A1 : OA1 = Ro.(V1²/2Vo²-V1²) (On retrouve le cas V1 = Vo.sqrt(2)).
Si eps est petit : l'énergie massique a peu varié : dE/m = mVo².eps . Puis dE/Eo = - da/Ro = -2/3 . dT/To . Donc OA1 = 4Ro.eps et l'excentricité est e = 2eps ; enfin dT = To.3eps
- - - - -
'''Fenêtre de tir :'''
Cette fois, l’Énergie massique n'a pas changé, donc le grand axe vaut 2Ro . Comme OMo = Ro , c'est l'extrémité du petit axe. donc k/\OMo donne la direction du grand axe. La projection de Mo sur celui-ci donne le centre de l'ellipse : l'excentricité vaut donc e = sin A ; d'où le périgée OP1 = Ro(1-sinA) : on ne peut se tromper que de 100 km :cela donne une fenêtre sin A = 100/7166 rad = 0.8°. Assez large , car les pointeurs donnent la seconde d'arc.
- - - - -
'''Erreur radiale :'''
Si eps = 1 , la trajectoire est parabolique !
Cette fois, le moment cinétique Lo est le bon ; donc le paramètre p est le bon . Donc OMo est perpendiculaire au grand axe , dont la direction est connue. Il est facile de calculer le vecteur excentricité qui donne en module eps.
On en déduit a = Ro/(1-eps²) (on retrouve eps = 1 comme limite).
----
'''Lâcher-Chute libre :'''
le Cours donne D = déviation vers l' Est de 2/3.wt.h .
Démontrons-le , façon Newton : la trajectoire est une ellipse , mais où r varie sensiblement comme R+h-1/2gt². La conservation du moment cinétique donne :
d<math>\theta</math>/dt = [(R+h)/R+h-z)]² .w = w (1+ 2z/R),
soit une déviation w.R. int(2z/R) = 1/3 w.gt².t = 2/3 wt.h
Si h= H , c'est l'exercice classique du géostationnaire :
R+H = R .17^(2/3) = 6.6 R = 42 000 km
Si h < H , il existe des trajectoires elliptiques dont Mo est l'apogée : la plus petite aura pour périgée OP = R , donc un grand axe 2a = 2R+H , d'où l'énergie massique . En posant r = Rx , on trouve x^4 + x^3 = 1/2 (289) , soit x = 4.67 et donc h = 3.67 R.
Si h > H , la pierre remonte ! résultat curieux qui aurait sans doute amusé Mersenne, et elle part à l'Ouest (si l'on ose dire).
enfin si h > H. 2^(1/3)= 8.36 R, alors E > 0 , donc trajectoire hyperbolique (limite : parabolique).
----
'''Balistique :'''
V= 8.2km/s := sqrt(gR) a signé le début de la Guerre Froide.
mais déjà les canons longue portée obligeaient à prendre une trajectoire elliptique et non parabolique : 111.111 km c'est déjà 1° à l'équateur!
1/. Si l'obus arrive à l'antipode B' , OB = OB' = paramètre p = Lo²/m²gR² = R soit u.sinA = 1 . (évidemment trajectoire avec A< 45° : il faut une apogée!)
2/. La portée s'évalue en calculant la direction du vecteur-excentricité 1 + i.Lo.Vo.exp(iA)/mgR² = [1-u²sin²A] +i[u²sinAcosA]=> tanB = 1/2 u².sin2A / (1-u²sin²A).
Pertinence : on retrouve Torricelli pour u <<1 ; et le §1.
3/. Pour B donné , équation en tan A :
tan²A (1-u²) - tan A (u²/tanB) + 1 = 0 d'où deux angles B1 et B2 tels que tan(B1+B2) = (tanB1+tanB2)/(1-tanB1.tanB2) = S/(1-P) = -1/tanB, donc A1+A2 = Pi/2+ B : il existe une trajectoire tendue et une plongeante. Portée maximale : tan B = u²/2(1-u²) [pertinent avec u=1 ]
'''Géométriquement''', tout ceci est relatif à la courbe de sûreté qui est l'ellipse de foyers T et B et d'apogée BH (rappel 1/H = 1/h -1/R , exercice sur l’énergie potentielle). En effet , toutes les trajectoires Tr(A) ont m énergie , donc m grand axe , soit TH+HB . Le lieu du deuxième foyer est donc le cercle [centre B, rayon BH]: pour une portée donnée (donc angle B donné , il y a deux solutions : à l'intersection de la droite d'apogée avec ce cercle ; soient F1 et F2 : alors la vitesse initiale étant bissectrice de TBF , les deux vitesses sont telles que A1+A2 = Pi/2+ B. La racine double est lorsque sinB = H/R ( = u²/(2-u²)).
L'ellipse de sûreté est donc telle que MT+MB = HO+HB, et dans ce cas, BM est corde focale [les raisonnements sont calqués sur ceux de Torricelli].
----
=== Exercices d'astronomie ===
==== Étoiles doubles ====
Montrer que dans le cas d'une étoile double, la troisième loi de Kepler s'écrit assez naturellement :
w² . a³ = G (m1+m2)
Que penser des planètes du soleil ?
'''Réponse :'''
Le problème à deux corps donne la réponse : (masse-réduite).w² a = G.m1.m2/a². Ainsi , on obtient une formule symétrique en m1 et m2 , ce qui est pertinent.
Dans le cas des planètes du Soleil , la plus grosse, Jupiter, n'apporte qu'une petite correction m2<< M(Soleil) , ce qui justifie la loi de Kepler. Pour les calculs précis, on fait les corrections, étant entendu que le barycentre du système solaire est quasiment en mouvement uniforme (pour plus de corrections, par exemple pour la ceinture de Kuiper ou le nuage de Oort, il faut envisager la "marée galactique").
==== Conjonction Mars -Terre ====
La distance T-Soleil = 1UA ,période 1an, excentricité e(T); mars-Soleil = d UA,période k ans, excentricité e(M). Montrer que '''TM''': = '''OD''' ne peut varier que dans une couronne >d1 et <d2. Le point D est-il dense dans la couronne? Si k était rationnel := p/q quel serait le mouvement de D.
'''Réponse :'''
Consulter exercices de l'IMCCE.
==== équant de Ptolémée ====
Soit une planète, disons Mars, de trajectoire elliptique d'excentricité e ( = 0.093).
On prend comme échelle de temps l'angle polaire compté à partir du deuxième Foyer F', où "il n'y a rien!".
Est-ce mieux ou moins bien que de compter theta(t) comme temps "uniforme" ?
'''Réponse :'''
Historiquement, cet exercice a beaucoup d'importance : on ne distingue pas un cercle d'une ellipse dès que e<0.1.
Donc Ptolémée croyait que la trajectoire était circulaire. MAIS il avait bien vu que theta(t) n'était pas uniforme ; par contre theta ' (t) l'était à la précision des mesures de l'époque. C'est ce que l'on demande de prouver.
----
== Rapatriement provisoire de la WP:historique de démonstrations ==
Ici est placé tout le travail de recherche historique qui n'intéresse pas forcément tout le monde : il y eût moult "démonstrations" du cours précédent.
=== Newton (1684) ===
*1/. '''la première''', celle de Newton en novembre 1684, est géométrique, le temps étant évalué par l'aire balayée (2ème loi de Kepler) : l'analyse en est faite dans l'[[Exégèse des Principia]].
=== Hermann (1710) ===
*2/. '''la plus simple''' (1710 & 1713) est celle de [[Jakob Hermann]] (1678-1733), élève de [[Jacques Bernoulli]] (1654-1705) : il écrit à [[Jean Bernoulli]] (1667-1748) : on remarque que l'hodographe est un cercle (notion de vecteur excentricité) : en calculant le produit scalaire '''e.r''', on trouve l'ellipse et son péricentre. L'analyse est faite dans [[Invariant de Runge Lenz]].
Laplace la reprendra dans son traité de « Mécanique Céleste ».
Que cela est vite dit dans notre langage moderne ! En réalité, la démonstration géométrique est la remarque classique sur le rôle des podaires dans le cas de champs centraux. Danjon remarque (avec Hamilton) que l'anti-podaire de l'inverse d'un cercle est une conique : cela était enseigné encore au baccalauréat des années 60 (Cf. LEBOSSÉ & EMERY, cours de mathématiques élémentaires).
Quant à Hermann, c'est un tour de force :
Il possède trois intégrales premières en coordonnées cartésiennes tirées de <math>\ddot{x} = -gR^2\cdot x \cdot r^{-3}</math> et idem en y.
* <math>C := x\dot{y} -y\dot{x}</math>
* <math>E_x := \frac{x}{r} - \frac{C}{gR^2}\cdot \dot{y}</math>
* <math>E_y := \frac{y}{r} - \frac{C}{gR^2}\cdot \dot{x}</math>
Éliminer la vitesse : on trouve <math> x \cdot E_x +y\cdot E_y = r- p </math> : c'est une ellipse (Cf.discussion [[conique]], Kepler).
Mais comment a-t-il trouvé les deux intégrales premières du vecteur excentricité ? par un raisonnement analytico-géométrique horriblement compliqué ! On sait aujourd'hui le faire par la théorie de la représentation linéaire des groupes (Moser et SO(4) :1968)
=== Transmutation de la force par Newton ===
*3/. '''la plus surprenante''' est celle de la [[Transmutation de la force]] (Newton, retrouvé par Goursat (1889)): ce théorème est EXTRAORDINAIRE et apprécié des ''afficionados'' des ''Principia''.
=== Keill (1708) ===
* 4/. '''la classique''' : Newton-Keill (en 1708) - Bernoulli (1719)
"Classique", elle est bien "chencitournée".
Le problème est plan, si la force est centrale. Le plan de phase est donc (<math> x,y,\dot{x} , \dot{y}</math>). Les deux équations du PFD (principe fondamental de la dynamique) sont :
<math>\ddot{x} = - \Omega^2 \cdot x</math>
et la même en y.[Évidemment <math>\Omega </math> dépend de r!].
Cette notation est évidemment très réminiscente de celle de Hooke. Mais elle n'a rien à voir, sinon que la symétrie est centrale.
Choisir trois fonctions invariantes par rotation :
*<math>I := 1 \cdot (x^2+ y^2) = r^2</math>, strictement positif,
*<math>J := 1 \cdot (x\dot{x} + y\dot{y})</math>, de sorte que <math> \dot I = 2J (= 2r\dot{r})</math>,
*<math> \ K := {v^2}/{2}</math>, énergie cinétique.
Remarquer cette particularité : r² est choisie comme variable, et non r. Et comme J est non-nulle, I va jouer '''le rôle d'une échelle de temps''' au moins sur une demi-période, du périgée à l'apogée.
Démontrer que le problème se réduit au système différentiel (S) :
*<math>\dot{I} = 2J</math>
*<math>2\dot{J} = K -I \Omega^2(I)/2</math> (th du viriel !)
*<math>\dot{K} = - J \Omega^2(I)</math> (loi de Newton!)
- - -
Keill utilise alors '''l'échelle de temps I''' ; le système se réduit à :
*<math> 4\frac{d(J^2)}{dI} = 2K - I \Omega^2</math>
*<math>\frac {dK}{dI} = - \Omega^2/2</math>
En éliminant Omega² (et quelle que soit sa valeur ! donc c'est vrai pour toute force centrale!)
<math> K = \frac{J^2}{2I} + \frac{C_o^2}{2I}</math>.
C'est un vrai ''tour de force'' : au début du XVIII{{ème}} , on vient de réécrire :
<math>2KI = v^2\cdot r^2 = [\vec r \cdot \vec v]^2 + [\vec r \wedge \vec v]^2 = [\vec r \cdot \vec v]^2 +C_o^2</math>
Emmy Noether connaissait-elle cette démonstration due à l'invariance par rotation ?
- - -
Puis, l'invariance temporelle donne la conservation de l'énergie :
<math>1.\cdot H = K + V(I)</math>, où V(I) est l'énergie potentielle relative à la force centrale (= <math>-\frac{1}{2}\int \Omega^2 dI)</math>
- - -
Ces deux ensembles de surfaces feuillettent l'espace (I,J,K) et leur intersection donne l'orbite du mouvement dans cet espace.
Éliminer K conduit à travailler dans le demi-plan (<math>I, 2J = \dot{I}</math>), c'est à dire dans un plan de phase presque usuel (on joue avec r² plutôt qu'avec r) :
<math> H = \frac{J^2}{2I} + \frac{C_o^2}{2I} + V(I)</math>,
ce qui est '''l'équation de Leibniz(1689)''', mais en notation I = r². (Remarquer que tout résulte de cette circonstance (non évidente du temps où les vecteurs n'existaient pas) :
<math>x \dot{x} + y \dot{y} = r \dot{r}</math>)
et pour finir, comme d'habitude, dt = dI/2J donne le mouvement sur cette orbite de phase et la primitive de 2J(I) donne l'action S(I) du problème.
Évidemment, actuellement, nous repasserions immédiatement en coordonnées (r et r').
Il n'empêche que voilà décrite la solution incroyable de Keill qui témoigne d'une virtuosité tombée dans l'oubli de l'Histoire.
*'''Note d'histoire''':
cette équation ayant été écrite par Lagrange sous cette forme, le H ne saurait signifier « valeur de l'Hamiltonien » ! Peut-être faut-il y voir un hommage à Huygens (?), premier à utiliser la généralisation du théorème de l'énergie cinétique de Torricelli ? peut-être est-ce une simple notation fortuite...
La suite est très classique et correspond à différents paramétrages dans le cas de Kepler :
L'équation de Leibniz se réécrit dans ce cas :
<math> H \cdot 8r^2 -4C_o^2 + 8(GM) \cdot r = 4J^2 </math>
qui est une conique en J et r, ellipse si H est négatif de grand axe <math>2a = - \frac{(GM)}{H}</math> :
Il est usuel alors de paramétrer via l' »anomalie excentrique » :
<math>r = a \cdot(1- e \cos{\phi})</math>,
et « miraculeusement » :
<math>\omega \cdot dt = \frac{r}{a} \cdot d\phi</math> ,
qui s'intègre en donnant la fameuse équation de Kepler.
En contrepartie l'équation en theta est légèrement plus compliquée à intégrer (primitive de <math>\frac{1}{r}</math>) d'où :
<math>tan \frac{\theta}{2} = \sqrt{\frac{1+e}{1-e}} \cdot tan \frac{\phi}{2}</math>.
Note de détail: certains préfèrent la notation i = I/2 , et/ou j = J/2.
=== Clairaut (1741) ===
*5/. '''la méthode de Clairaut''' (1741), reprise par Binet consiste à écrire l'équation de Leibniz à l'aide de u := 1/r :
<math> \dot{r}^2 = 2H + 2gR^2 \cdot u - C^2 \cdot u^2</math>
et cette fois le paramétrage adéquat est :
<math>u := 1+e\cdot \cos \alpha</math> et <math> \dot{r}: = e\cdot \sin \alpha</math>
ce qui conduit au « miraculeux » <math>d \theta = d\alpha</math> ! la trajectoire est donc une ellipse.
Mais la deuxième intégration conduit à <math>dt = k d\alpha \cdot 1/u^2</math> plus difficile à intégrer (mais tout à fait faisable !)
=== Lagrange (1778) ===
*6/. '''la méthode de Lagrange''' est originale (1778) et n'utilise que la linéarité de F = m.a !
Partant de l'équation radiale de Leibniz(1689) :
<math>\ddot{r} = C^2 u^3 - e^2u^2</math>
il pose comme nouvelle variable z = C²-r et trouve :
<math>\ddot{z} = -(GM) \cdot z \cdot u^3 </math>,
'''identique''' aux deux équations de départ en x & y !!
donc il obtient : z (:= C²-r) & x & y linéairement liés, ce qui est la définition d'une ellipse (Cf. [[conique]], discussion). CQFD
=== Laplace (1798) ===
*7/. ''' Laplace''', sans citer Lagrange, calcule, en force brutale, sans aucune intégrale première, l'équation en I = x² + y² du troisième ordre issue du système de Keill : d'où il tire
<math>\frac{d^3I}{dt^3} = - \frac{\dot I}{I^{3/2}}</math>
(comme quoi , le jerk ne date pas d'hier!)
Laplace en tire cette fois '''quatre''' équations '''linéaires''' identiques :
d/dt(r^3.Z") = - Z', avec Z = r, x, y, constante.
D'où r = a x + by + c.constante : c'est une conique !
Il reste à trouver une interprétation physique à ce calcul!
=== Hamilton (1846) et autres ===
*8/. Soit une ellipse ; le foyer F et sa polaire, la directrice (D). Soit P le point courant de l'ellipse et PH sa projection sur la polaire. Le [[théorème de Newton-Hamilton]] donne immédiatement la force centrale F ~ r/PH^3 soit ~ 1/r².e³.
*9/. Hamilton démontre aussi que pour toute mouvement sur une ellipse de paramètre Po, on obtient |'''a/\v'''|.Po = C^3/r^3. Donc si le mouvement est central de foyer F, |a/\v| = a.C/r d'où a ~ 1/r².
*10/. Hamilton est aussi le promoteur du renouveau de la méthode de l'hodographe circulaire que Feynman reprendra à son compte dans ses « lectures on Physics »
*11/ Hamilton va inspirer le [[Théorème de Siacci]] et puis Minkovski qui donnera beaucoup de propriétés des ovales : ceci donne encore une autre démonstration.
=== Goursat et régularisation dite de Levi-Civita ===
*12/. Goursat (1889), Bohlin(1911), AKN {Arnold & Kozlov, Neishtadt} reprennent la méthode z-> sqrt(z) = U (complexe) et le changement d'échelle de temps (dit de Levi-Civita ou de Sundman) dt/dT = 4 |z| : quelques lignes de calcul donnent via le théorème de l'énergie cinétique :
|dU/dT|² = 8 GM + 8 E |U|² ; soit par dérivation
<math> {d^2U \over dT^2} +(-8E)\cdot U = 0</math>, avec E négatif.
Donc U décrit '''une ellipse de Hooke''' et z =sqrt(U) l'ellipse de Kepler.
On aura reconnu en T(t), l'anomalie excentrique. Ce n'est donc qu'une des méthodes précédentes : mais cette méthode a des prolongements plus importants (Cf. [[théorème de Bertrand]]). Voir aussi plus bas.
===régularisation===
cette transformation du problème de Kepler en problème de Hooke est assez stupéfiante. Saari(p141) s'y attarde un peu plus qu'Arnold (Barrow,H,H,Newton) ; peut-être est-ce justifié ; voici :
Le problème de régularisation se pose s'il y a collision , c'est à dire , C très voisin de zéro. Saari dit : la collision entraîne un changement brutal de 2Pi . Afin de garder la particule sur la droite sans singularité , il "suffit de penser" à garder l'arc -moitié ; soit
de changer de jauge (de fonction inconnue): <math>\ U = \sqrt z</math> et de variable (transmutation d'échelle de temps) dT = dt/r(t)(ATTENTION au facteur 4!)
La conservation de l'énergie s'écrit 2|U'|²-1 = Eo.r
et l'équation du mouvement : <math>\ddot z = -z/r^3</math> devient :
<math>r \frac{d^2z}{dT^2} - \frac{dr}{dT}\cdot \frac{dz}{dT} +z = 0</math> ,
équation LINÉAIRE sans le r^3 ! Elle conduit à :
U" -U/r [2|U'|²-1] =0
soit <math>\frac{d^2U}{dT^2}+ (-Eo/2)\cdot U = 0</math> (équation de Hooke).
Le gros avantage de cette solution est qu'elle est stable-numérique : les solutions restent sur la même iso-énergie.
===Kustaanheimo(1924-1997) et Stiefel(1909-1978)===
en 1964, ils utilisèrent les quaternions pour transformer le problème de Kepler dans R^3 en celui de Hooke dans R^3, via R^4! (congrès d'Oberwolfach): ils leur a suffi de prendre la quatrième coordonnées x4 = cste : alors le quaternion U se déplaçait sur la sphère; ceci mit en exergue la symétrie SO(4) et mieux SO(4,2) qui correspondait à la version spinorielle du problème de Kepler (liée à la solution en coordonnées paraboliques) et mettait en avant le vecteur excentricité. Immédiatement, le traitement des perturbations fût amélioré (Stiefel et Scheifel,1971), mais aussi la quantification (methode dite de Pauli (SO(4)), et surtout la quantification lagrangienne SO(4,2),avec ses orbitales "paraboliques" de Kleinert (1967-1998)(cf Kleinert 2006).
*Saari donne des '''raisons topologiques à l'obstruction du passage de R^2 à R^3''' et la nécessité de passer à R^4 (les quaternions): la relation U^2 = z , ne pouvait se régulariser sur la sphère à cause du célèbre théorème du hérisson de Brouwer-Poincaré. Mais si on ne peut "peigner" S2 , on peut peigner S3 (et même S7:octonions), ce qui avec les trois vecteurs tangents donne la fameuse matrice 4-4 de la transformation K-S : rappelons que le maître de Stiefel était Hopf lui-même qui dressa la carte de S3 vers S2 : il n'y a pas de hasard, posséder une bonne formation, cela sert! (cf Oliver(2004)).
----
Voilà donc 12 démonstrations assez mal connues. En existe-t’il d'autres, de cette époque ?
Bien sûr, ont été exclues ici toutes les méthodes de mécanique lagrangienne et hamiltonienne, en particulier celle de [[Max Born]] (cf plus bas).
== Équation du temps, de Kepler : résolution ==
Dans le [[mouvement keplerien]], l''''[[équation du temps, de Kepler]]''' relie l'[[anomalie moyenne]] M = nt à l'[[anomalie excentrique]] E par l'équation
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> M = E - e \cdot \sin E</math>
|}
|
| |
|}</div>
où e est l'[[excentricité orbitale|excentricité]] de la planète.
'''Résoudre cette équation, c'est trouver E(e,M) :'''
* comme série de Fourier puisque c'est une fonction périodique impaire de M
* comme série de puissance de e, si e < eo := 0.6627..., rayon de convergence de la série.
* comme une valeur numérique avec un nombre de chiffres (d), pour un temps de calcul tc(d) optimisé.
=== Série de Fourier ===
C'est [[Joseph-Louis Lagrange|Lagrange]] qui trouve l'expression, bien que le nom J<sub>n</sub>(x) soit associé au nom de [[Friedrich Wilhelm Bessel|Bessel]].
* E-M = fonction impaire périodique de M :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = e\cdot sinE = 2 \cdot \Sigma_{n=1} \frac{J_n(ne)}{n} \sin(nM)</math>
|}
|
| |
|}</div>
'''Démonstration :'''
On rappelle la définition de Jn(z) :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> J_n(z) = \frac{1}{2\pi}\int_0^{\pi} \cos(nx-zsinx)dx</math>
|}
|
| |
|}</div>
et le développement classique de 1/[1-e.cosE] -1 , fonction paire périodique de moyenne nulle vaut:
<math>\Sigma a_n \cdot cos (nM)</math>
avec <math> \ a_n = 2 J_n(ne)</math>
car <math>\pi a_n = \int_0^{2\pi} cos (nM)/(1-e cos E)\cdot dM = \int_0^{2\pi} cos(n[x-esinx]) dx </math>
*On reconnaît (a/r)-1 = 2<math> \Sigma_{n=1}J_n(ne)\cdot \cos (nM)</math>
=== Série entière de l'excentricité ===
C'est encore Lagrange qui trouve la solution en inventant pour l'occasion son théorème d'inversion des fonctions holomorphes ; et [[Pierre-Simon Laplace|Laplace]] donnera le rayon de convergence : mais [[Augustin Louis Cauchy|Cauchy]], pas content du tout, fonde la théorie des séries analytiques pour résoudre ce problème épineux, qui verra son aboutissement avec les travaux de [[Victor Puiseux|Puiseux]].
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = \Sigma_{n=1} {e^n \over n!}\cdot a_n(M)</math>
|}
|
| |
|}</div>
avec <math>\ a_n(M) = D^{n-1} (\sin^n M)</math> et D := opérateur dérivée.
C'est l'application du théorème d'inversion de Lagrange.
*Le rayon de convergence de la série est : eo = 0.6627434193
{{Boîte déroulante|titre= note historique |contenu=
indiqué par Laplace ([[1823]]) et démontré par Cauchy et Puiseux : eo = '''max (x/chx)''' ; soit eo = 1/sh(xo) avec 1/xo = th(xo);démonstration in Wintner, sur l'analyticité de la série.}}
==== Cas des comètes : {{MathText|e > \ e_o|e > eo}} ====
Le premier à se confronter au problème est [[Jeremiah Horrocks|Horrocks]], puis surtout [[Edmond Halley|Halley]] ([[1705]]), pour les calculs sur sa comète d'excentricité e = 0,9673.
Il faut modifier légèrement la solution de Barker (e = 1). Et Bessel([[1805]]) résout ce cas, mais pour e > 0.997
[[Carl Friedrich Gauss|Gauss]] ([[1809]]) s'illustra en donnant une belle solution pour 0,2 < e < 0,95
Autant dire que le voisinage de (0,95 ; 0,98) est fertile en problèmes, en cas d'itération !
=== Calcul numérique ===
Les calculs via les [[intégrateur symplectique]]s exigent de rester toujours en butée du nombre de digits, dans le moindre coût de calcul.<br/>
Depuis 300 ans, on cherche la « meilleure » méthode. Elle reste à trouver !
Bien sûr, cela dépend beaucoup du doublet (M,e), M compris entre 0 et Pi et de e, surtout quand e est voisin de 1.
Nijenhuis (1991) adopte la méthode de Mikkola (1987) qui est la méthode de Newton d'ordre 4, en choisissant « adéquatement » le germe Eo en fonction du doublet (M,e).
Il est clair que dans les calculs numériques, le volume de calculs est essentiel, autant que le nombre de décimales, vu l'instabilité du système solaire évaluée à un [[exposant de Lyapunov|coefficient de Liapunov]] de 10^(t/5Myr). On se heurte à une muraille exponentielle : difficile d'aller plus loin que 25 Myr, même avec un traitement 128 bits.
Ce sont ces calculs (astronomiques... mais informatisés) qui tournent sur les machines de l'IMCCE-Paris. Le calcul de l'ensoleillement terrestre à la latitude 65°Nord, I(65,t) est calculé et on essaie d'en déduire la corrélation avec le climat passé : l'échelle géologique jusqu'au Néogène (25M ans) en est déduite(échelle géologique Gradstein 2004). Prochaine étape prévue : les 65 M ans.
=== Histoire des sciences ===
Avant Kepler, l'équation est déjà étudiée ! bien sûr, pas pour le même problème, mais pour la même équation :
c'est le problème de la réduction des coordonnées locales aux cordonnées géocentriques : il faut réduire la correction de parallaxe. Habash al Hasib s'y est déjà attaqué.
Avant 1700, il y a déjà beaucoup de tentatives : Kepler naturellement, Curtz (1626), Niele, [[Ismaël Bouillau|Bouillau]] (1645, 1657), [[Seth Ward]] (1653), Paganus (1657), Horrebow (1717), [[Jean-Dominique Cassini (Cassini I)|Cassini]] (1669), Newton (1665?), [[Christopher Wren|Wren]] (1658), [[John Wallis|Wallis]] (1659),... De toutes, celle de [[Jeremiah Horrocks]] (1638) est de plus grande beauté. Cf le Colwell, déjà cité.
==== compléments ====
En 1770, Lagrange trouve les deux séries, mais le changement des termes dans les séries le laisse perplexe. 1821 : Cauchy enfin ! Sitôt après, 1824, [[Bessel]] (1784-1846)fera une étude extensive de "ses" fonctions , déjà apparues en 1703 dans une lettre de jean Bernouilli à Leibniz. Daniel Bernouilli fait la théorie du mode propre de la corde suspendue et introduit Jo(x); Euler généralisant a besoin des In(x) , les bessel-modifiées.
*Les calculs de développements approchés donnent :
* E-M = e.sin M[1-e^2/8 +1/192 e^4]+(e²/2). sin(2M)[1-e^2/3 +e^4/24] +e^3.sin (3M)[3/8 -27 e²/128] +e^4/3 .sin(4M)[1-4e²/5] + 125 e^5/384 . sin (5M) + 27 e^6/80 .sin (6M) +O(e^7) (p202 Battin)
* OM/a = 1 - e cos wt +e²/2(1- cos(2wt)) + 3/8e³[cos(wt)-cos(3wt)] + 1/3e⁴[cos(2wt)-cos(4wt)]+ O(e⁵)
* angle POM = θ(t) = wt +2e sin(wt) +5/2 e² sin(2wt) + e³[13/12sin(3wt) -1/4 sin (wt)] +e⁴ [103/96 sin (4wt) -11/24 sin(2wt)] + O(e⁵).
*La solution d'Horrocks(1638) fût :Translater Delphine du déférent de (-2c,0) en D' et prendre E = angle (CP,CD')où C est le centre du déférent
On montre que E(Horrocks) = M + e/1sin M +e²/2 sin 2M +e³/3 sin3M +... et E(H) -E = 1/6 .e³sin³M ; pas si mal!
*La méthode la plus simple est évidemment "regula falsi" (interpolation linéaire inverse ou méthode dite de l'artilleur):
la fonction étant croissante , on "tire" trop bas avec x0 (F(x) est négatif), trop haut avec x1 (F(x) est positif) : alors la racine est entre les deux et on prend la corde.
* On peut montrer que E-M satisfait l'équation cartésienne de Newton : en effet c'est e sin E et donc proportionnelle à y(E)
* (Gudermann(1798-1852)): le cas des orbites hyperboliques se traite par Corinne et donc le Gudermannien :
x = a ch u et y = b sh u ; r = a(1-e ch u)
On pose 1/cos g = ch u et tg g = sh u
soit g = gudermannien (u) = gd(u) = 2 arctg(exp u) -Pi/2.
* Sundman (1873-1949)introduisit en 1912 le temps régularisant :
=== Voir aussi ===
*[[mouvement keplerien]]
*[[intégrateur symplectique]]
*[[Jeremiah Horrocks]], cf discussion.
*Colwell (1993) : solving Kepler's equation over three centuries, ed Willmann-Bell, {{ISBN|0-943396-40-9}}
*Brinkley (1803) : trans roy irish ac, 7,321-356.
== Après Lagrange, jusqu'à Born-Sommerfeld ==
== Les transformations hamiltoniennes du problème de Kepler et SO(4) ==
== En attente , les perturbations , pour faire de mon mieux ==
les perturbations du mvt de Kepler sont parmi les plus "dures" car il y a la dégénérescence banale de SO(3), mais aussi la dégénéréscence de SO(4) pour les états liés : du coup il faut comprendre la structure de la sphère S3 dont on sait qu'elle se retourne comme un gant ou peut se transformer en une foliation torique de Hopf, etc. Comment la perturbation agit sur chacun de ces aspects est encore à inventorier, même si on en connaît pas mal sur le sujet, en particulier gràce aux travaux de Poincaré, KAM, Mather, etc. Il est vrai que le niveau est plus élevé ici, puisqu'il s'agit de problèmes le plus souvent non intégrables.
=== à la manière directe : Danjon-Pollard-Duriez ===
la perturbation F est installée au temps t=0 , avec OMo et Vo donnés , càd Lo,Eo et eo données et passage au périgée donné.On appellera '''ko''' la direction de Lo, et '''uo''' = '''OMo'''/ro, et <math>\vec{u_{\theta_o}}</math> pour compléter le trièdre, dont le vecteur-rotation instantanée sera <math>\vec{\Omega_o}</math> ( '''v''' signifiera donc '''vecteur-vitesse'''). Sept équations sont bien compréhensibles :
* <math>\dot{\vec{L}} = \vec{OM}\wedge \vec{F}</math> (théorème du moment cinétique)
* <math> \dot{E} = \vec{v}\cdot\vec{F}</math> (théorème de l'énerie cinétique)
*<math> \dot{\vec{e}} = \vec{F}\wedge \vec{C} + \vec{v}\wedge \vec{C} </math> (théorème du moment"excentricité")
Moins évidente est la variation de l'anomalie moyenne :
*<math> \omega \cdot a^2 \cdot \dot{M} + \vec{\Omega} \cdot \vec{C} = -2E -2\vec{OM}\cdot \vec{F} </math> que l'on "extrait" du viriel en force.
Il en résulte les équations de Gauss.
==== équations de Gauss ====
le quintuplet [a,e,i,<math>\Omega, \omega </math>]s'en déduit projeté sur le reférentiel initial et final :
* <math>C \cdot \dot{a}= 2a^2\cdot\vec{F}(\vec{u_{\theta} }+e \vec{u_{\theta_o}})</math>
* <math> C \cdot \dot {e} = r (e+cos\theta) \vec{F}\cdot \vec{u_{\theta}}+ p \cdot \vec{F}\cdot \vec{u_{\theta_o}}</math>
* <math> C \cdot (\dot{\omega} +cos( i) \cdot \dot{\Omega}) = r sin \theta \cdot \vec{F}\vec{u_{\theta}} -p \cdot \vec{F} \cdot \vec{u_o}</math> et
* <math>C \cdot (sin (i)\cdot \dot{\Omega}) = r \cdot sin(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math>
* <math> C \cdot\dot{i} = r cos(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math> et bien sûr C varie comme :
*<math>\dot{C} = r \cdot \vec{F}\vec{u_{\theta}} </math>
Et il reste encore dM/dt à écrire !
Comme de plus il faut projeter l perturbation sur la base initiale et la base finale , l'interprétation est sévère.
heureusement, la perturbation dérive souvent d'un potentiel : cela simplifie l'écriture et la compréhension de ces 6 équations, sur lesquelles il faut se pencher qq temps pour les assimiler.
==== pertinence des équations de Gauss ====
demandée ici, pour "souffler un peu" : le cours est construit ainsi ! ne rien faire que l'on ne puisse refaire ou retenir ! Pour retenir, il faut manipuler et croiser les équations jusqu'à ce que cela devienne "machinal" et au fond "intuitif" . Donc la question posée est : en quoi les 6 équations précédentes vous semblent-elles pertinentes ?
{{Boîte déroulante|titre= pertinence des équations de Gauss ; dissertation en 3heures | contenu= d'adord et toujours l'homogénéité ! ensuite prendre des cas particuliers "évidents", etc. }}
=== Perturbation de Kepler : effet Stark classique ===
Si à la force newtonienne vient se rajouter une petite force F, la trajectoire va être légèrement perturbée. Néanmoins si F est parallèle au vecteur excentricité, la symétrie ne sera pas entièrement détruite.
Il convient de prendre les bonnes coordonnées pour traiter ce problème. Comme on sait traiter le mouvement keplerien en [[système de coordonnées paraboliques]], il faut évidemment en profiter.
Mais si F devient trop grand, il apparaît clairement que l'atome va pouvoir s'ioniser plus facilement.
En mécanique quantique cela sera encore plus évident via l'effet tunnel, conduisant à l'ionisation Stark, fragilisant surtout les [[atome de Rydberg]].
=== Mouvement d'Euler à 2 centres d'attraction ===
Euler a vite compris que la composante du vecteur excentricité permettait d'intégrer le problème à 2 soleils fixes et une planète. Cela s'opère grâce à un [[système de coordonnées bifocales]].
Vinti s'est fait le promoteur de cette méthode : ébauche
=== Mouvement si Terre-galette (Béletskii) ===
Beletskii a fait remarquer que le problème d'Euler pouvait s'appliquer à un Soleil légèrement allongé de forme cigare. Par prolongation analytique, avec des masses « imaginaires », il a proposé une interprétation simple du mouvement d'un satellite terrestre sous l'action perturbante du bourrelet (le terme J2(P2(cos(theta)/r³) dans le potentiel gravitationnel. On retrouve les effets décrits dans [[satellite artificiel]].
=== Perturbation de Kepler par planète proche : Terre & Lune ===
Ce problème est ardu : Newton disait que cela lui donnait mal à la tête.
Il a fallu attendre Clairaut (1741) pour avoir une première théorie de la Lune.
Aujourd'hui avec les miroirs posés sur la Lune (Apollo et Lunakhod), on peut comparer la théorie analytique à celle numérique. La précision théorique des LLR (laser lunar range: tir laser vers la Lune) est de quelques centimètres. La théorie analytique comprend plusieurs milliers de termes, mais donne aussi une précision de quelques mètres.
à compléter (séminaire Laskar du 09/03/06).
=== Perturbation de Kepler par planète lointaine : Terre & Jupiter ===
Là, le problème est plus facile . L'essentiel de la méthode consiste en une méthode variable rapide- variable lente, due à Legendre, puis Gauss.
à compléter.
=== Perturbation de Kepler et symétries ===
Bien sûr, chaque fois qu'un système possède une symétrie continue, le théorème de Noether donne une intégrale première, ce qui permet d'éliminer une variable de l'espace des phases.
Comment s'opère cette réduction ?
Le livre de Cordani, celui de Marsden & Ratiu expliquent cette réduction.
Enfin, le problème garde toujours sa symétrie symplectique : il faudra expliquer comment fonctionnent les [[intégrateur symplectique]] (Laskar & Robutel, Celestial Mechanics, 2001,80, 39-62).
------------
== Applications ==
Elles sont innombrables :
*les principales historiquement sont celles de l'astronomie, et prosaïquement des éphémérides solaire et lunaire de notre calendrier des postes.
*les plus utiles sont celles des satellites artificiels.
* le modèle de Rutherford-Bohr de l'atome s'appuie sur cette théorie.
== Perturbations du mouvement de Kepler ==
C'est évidemment essentiel.
Pour les satellites artificiels, il faut tenir compte de la forme non sphérique de la Terre , ET de toutes les autres petites perturbations ( pression de radiation du Soleil sur les panneaux solaires, action de gravité différentielle de la Lune et du Soleil, etc.
Pour l'astronome , il y a essentiellement deux problèmes :
* la perturbation du mouvement Terre-Lune dû au Soleil
* la perturbation de Saturne par Jupiter.
À l'heure actuelle, les programmes de calculs peuvent envisager de traiter (sur un temps pas "trop grand") le mouvement de l'ensemble des planètes. On sait depuis peu que Pluton n'est pas une vraie planète. Ceci dit, le mouvement des planètes sur des échelles de qq 10^6 années commence à être sensible aux conditions initiales (la Terre est un cas particulier car la Lune vient stabiliser son inclinaison et son excentricité).
Pour le programme [[w:Galileo (système de positionnement)|Galileo]] (le [[w:GPS|GPS]] européen), la précision sur le positionnement de la constellation de satellites artificiels est assez impressionnante(inférieure au centimètre).
----
----
== insert provisoire:atome d'hydrogène ==
Cet article suit l'article [[atome d'hydrogène]].
La résolution de l'équation de Schrödinger, écrite en coordonnées polaires, se découple des variables (<math>\theta, \phi</math>) et conduit à une équation à une dimension en r, appelée équation radiale de Leibniz-Schrödinger, puisque ce n'est jamais que la célèbre équation de Leibniz de 1685 traduite en mécanique quantique.
Mais l'équation de Schrödinger (1926) peut se résoudre autrement comme Pauli l'a montré en 1925 !
== Équation radiale ==
L'équation radiale 1D de Leibniz-Schrödinger s'écrit pour r>0:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>-{\hbar^2 \over 2m}S^{''} + ({\hbar^2l(l+1) \over 2r^2} - {e^2 \over r} )S(r) = E \cdot S(r)</math>
|}
|
| |
|}</div>
avec E valeur propre négative ,
et S(r) s'annulant "vite" à l'infini, et S(0) =0 :il s'agit donc d'un problème aux limites dit de Sturm (par opposition à un problème aux conditions initiales, dit de Cauchy); de plus <math>\int_0^{\infty} S^2(r) dr = 1</math>.
[On reconnaît dans <math>{ \hbar^2 l(l+1) \over 2m r^2} </math> la barrière centrifuge de Leibniz (l entier positif) (l=0 correspond à L =0 ; le problème classique n'a pas de correspondant simple en mécanique quantique, encore que ...)].
*Comment arrive-t-on à cette équation '''radiale''' de Leibniz-Schrödinger ?
Il SUFFIT de chercher la fonction d'onde <math>\Psi(x, y, z, t)</math> en coordonnées sphériques sous la forme :
* <math>\Psi = {S(r) \over r}\cdot Y_{l,m} (\theta, \phi) e^{-i{Et \over \hbar}}</math> ,
où les Y(l,m) sont les fonctions [[harmoniques sphériques]]. On appelle ce procédé courant dans les équations aux dérivées partielles, la séparation des variables. Souvent, on appelle R(r) := S(r)/r , la partie radiale de la fonction d'onde.
*'''Note importante annexe''' :
=== Harmoniques sphériques ===
Il n'y a '''rien de mystérieux''' (et surtout rien à voir avec la MÉCANIQUE quantique) dans ce qui semble être un tour de passe-passe. L'étude en électrostatique '''classique''' de l'opérateur Laplacien conduit à ces mêmes fonction Y(l,m) , appelées [[harmoniques sphériques]], qui sont des fonctions '''usuelles''' dès que la symétrie sphérique entre en jeu. L'entier relatif m ne peut prendre que 2l+1 valeurs, de m = -l à m = +l , l étant un entier positif.
Ce sont ces harmoniques sphériques qui "quantifient" le problème sphérique par les deux nombres quantiques l et m (comme il est '''usuel''' dans tout problème de Sturm, dit "aux limites", des équations différentielles), ces deux entiers l et m qui auront tant d'importance dans l'étude de l'[[atome à N électrons]] et donc de la [[Classification périodique]].
*Pour rester en continuité de lecture(sinon voir l'article [[Harmonique sphérique]]), est expliqué ici juste le minimum pour comprendre comment elles interviennent à ce niveau modeste (l=0,1,2,3):les (2l+1)polynômes <math>r^l Y_{l,m}</math> forment une base sur l'ensemble des polynômes homogènes P(x,y,z) de degré l, harmoniques(c’est-à-dire dont le laplacien est nul)
*l=0 :<math>Y_{0,0}= {1\over sqrt(4\pi)}</math> : c'est bien un polynôme de degré zéro, normé sur la sphère unité puisque son carré vaut 1/4Pi.
'''''Dorénavant, nous n'indiquerons plus ce facteur dit de normalisation'''''.
*l=1 :3 fonctions
<math>rY_{1,0} = rcos \vartheta = z</math> ;
<math>rY_{1,1}+rY_{1,-1} = 2rsin \vartheta cos\varphi =2x </math>;
<math>rY_{1,1}-rY_{1,-1} = 2irsin \vartheta sin\varphi =2iy </math>;
soit la base {x,y,z} dite orbitales <math>p_x</math>, <math>p_y</math>, <math>p_z</math>
----
*l=2: cinq fonctions
<math>r^2Y_{2,0} = r^2(3cos^2 \vartheta -1) = 2z^2-x^2-y^2</math> ;
<math>r^2Y_{1,1}+r^2Y_{2,-1} = 2r^2sin \vartheta cos\vartheta cos\varphi = 2xz</math>; et avec moins , 2i yz ;
<math>r^2Y_{2,2}-r^2Y_{2,-2} = 2ir^2sin^2 \vartheta sin2\varphi =4i xy </math>;
<math>r^2Y_{2,2}+r^2Y_{2,-2} = 2ir^2sin^2 \vartheta cos2\varphi =4(x^2-y^2) </math>;
Soit la base {3z^2-r^2, xz, zy, yx, x^2-y^2) dont chaque fonction est de laplacien nul.
----
*l=3: 7 fonctions
soit la base { z(5z^2-3r^2), x(5z^2-3r^2), y(5z^2-3r^2),zxy,z(x^2-y^2),x(x^2-y^2), y(x^2-y^2)}dont chaque fonction est de laplacien nul.
----
* l quelconque : on trouve une base de (2l+1) polynômes réels, mais bien sûr toute combinaison linéaire complexe reste dans ce sous-espace vectoriel sur le corps des complexes.
{{Boîte déroulante|titre=Pourquoi (2l+1)?|contenu=la raison en est aisée :effectuons le décompte : il y a (l+1)(l+2)/2 polynômes homogènes de 3 variables (c'est le nombre de manières d'avoir avec un triplet d'entiers{m,n,p]avec la relation m+n+p = l). Quand on calcule le laplacien on tombe sur l'espace des polynômes homogènes de degré (l-2),de dimension (l-1)l/2 ,pour l >1 ce qui donne pour l'annulation du Laplacien autant de conditions. Donc il ne reste, pour les polynômes homogènes harmoniques qu'un sous-ev de dimension (l²+3l+2 -l²+l)/2 = 2l+1.}}
*Théorème: <math>{P_l(x,y,z) \over r^{l+1}}</math> est fonction propre du laplacien avec la valeur propre -l(l+1):
C'est ce théorème qui est sans arrêt utilisé pour la théorie de l'atome d'hydrogène.
En chimie ,on représente souvent les fonctions 1/r^(l+1) . Pl comme les harmoniques sphériques des orbitales l ; parfois on prend leur carré; etc.
Dans l'[[atome à N électrons]] pour N< 119, l< 5 : donc cela suffit au physicien de l'atome, qui leur a donné des noms et des représentations mnémotechniques diverses. Ne pas oublier que l'on peut combiner à volonté ces fonctions, pour former ce que les chimistes appellent des orbitales hybridées du sous espace propre du niveau d'énergie En( en particulier les fameuses orbitales paraboliques de Kleinert).
=== Multiplicité (2l+1) ===
Le nombre quantique l est appelé '''nombre quantique azimutal''' (on voit qu'il joue, par son terme l(l+1), le même rôle que le carré du moment cinétique, L², en mécanique classique). Évidemment l'équation radiale a ramené le mouvement à UNE seule dimension, la variable radiale, avec la fonction S(r) qui doit s'annuler en r=0 (n'oublions pas c'est S(r)/r qui intervient ) et qui doit être de carré sommable sur l'intervalle r>0 .
On aura donc des valeurs propres de cette équation linéaire, dépendant donc de l , <math> E_{k, l}</math> , mais pas de m (on dit que la multiplicité de la valeur propre est : 2l+1 ; en physique & chimie on dit : il y a dégénérescence du multiplet égale à 2l+1).
Le nombre quantique m s'appelle '''nombre quantique magnétique''', car sous l'effet d'un champ magnétique ([[effet Zeeman]]) l'énergie dépend alors de la valeur de m, et l'on voit une multiplicité de niveaux d'énergie, d'où la dénomination .
Enfin le nombre k , entier positif, s'appelle '''nombre quantique radial''' et donne le nombre de nœuds (k pour knots !) de S(r) pour r > 0 .
Comme la spectroscopie est née un siècle avant la mécanique quantique, la tradition est restée d'appeler le nombre quantique azimutal l par des lettres latines :
l= 1 -> s ; l=2-> p ; l=3 -> d ; l=4 -> f et ensuite g, h .
=== Résultat final ===
Au final, on trouve une énergie E(l,m,k) indépendante de m, soit E(l,k), mais, de façon incroyable (sauf pour Pauli), ne dépendant que de la somme l+k-1 = n , qui doit être un entier positif, et appelé '''nombre quantique principal'''.
C'est la fameuse équation déjà trouvée par Bohr en 1913:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>E_n = {E_1 \over n^2}= -{me^4 \over 2n^2\hbar^2}</math>
|}
|
| |
|}</div>
Il y a ce qu'on appelait une '''dégénérescence accidentelle''', avant l'introduction par Pauli en mécanique quantique du vecteur [[invariant de Runge Lenz]].
La multiplicité, g, du niveau d'énergie En est donc :
pour l variant de 0 à n-1 et
pour m variant de -l à +l
<math> g =\Sigma_0^{n-1} (2l+1)= n^2</math> .
Et, compte-tenu du spin (1/2) de l'électron ,g vaut le double , soit 2.n²
*Ce qui donne simplement : couche K, g=2 ; L, g=8 ; M, g=18 ; O, g= 32 ; P, g = 50 ; Q, g=72 ; R, g = 98 ; S, g= 128.
Inutile d'aller plus loin pour décrire la [[classification périodique]], la configuration de l'élément Z= 119 est celle d'un alcalin :
<math>(1s^2) (2s^2) (2p^6) (3s^2) (3p^6) (4s^2) (3d^{10}) (4p^6)</math> soit Kr(Z=36) puis,
<math>(5s^2) (4d^{10}) (5p^6) (6s^2) (4f^{14}) (5d^{10}) (6p^6)</math> Rn(Z=86) , puis
<math>(7s^2) (5f^{14}) (6d^{10}) (7p^6)</math> Uuo(Z=118),
puis 8s.
Sur les 64 orbitales de la couche S, n= 8 , on n'a besoin de connaître que l'orbitale (8s): ce calcul requiert impérativement la [[mécanique quantique relativiste]] , car les électrons (1s²) de la première couche sont soumis à des vitesses non négligeables devant c .
De même, la configuration de l'élément Z= 121 est Uuo,(8s²,5g), la sous-couche 5g pouvant contenir jusqu'à 2*9 =18 électrons.
- -
Ce faisant, on obtiendra ainsi tous les niveaux d'énergie des éléments '''ET des séries isoélectroniques''', ce qui permettra de décrire certains traits de la [[classification périodique]].
* Pour en revenir à l'atome d'hydrogène, il ne reste plus qu'à introduire le vecteur [[invariant de Runge Lenz]] quantique pour comprendre que la dégénérescence dite "accidentelle" ne l'est pas : il y a bien une symétrie de plus que la simple symétrie centrale dans le cas de ce modèle de Rutherford quantique (cf [[théorème de Bertrand]]).
Auparavant, on va finir le raisonnement de Schrödinger (1926) ; puis on reviendra sur celui, plus subtil, de Pauli (1925).
=== Équation radiale-réduite et Polynômes de Laguerre ===
Si l'on revient à l'équation radiale de Leibniz-Schrödinger, on peut démontrer que pour r voisin de zéro, S(r) varie comme r^(l+1) , et que pour r très grand, S" + 2E S = 0 .
Il est courant de poser 2E = -1/n² et donc S(r) varie comme exp (- r/n) à l'infini : n pour l'instant n'est qu'un réel!
Alors le dernier changement de fonction inconnue est logiquement l'essai suivant qui se révèle fructueux : S(r) = r^(l+1).exp(-r/n).g(r) ; mais on s'aperçoit qu'en changeant la variable r en s : = 2r/n l'équation s'arrange mieux :
L'équation radiale-réduite devient :
s f"(s) + (2l+2-s) f'(s) + (n-l-1) f(s) = 0 , avec g(r) = f(2r/n) = f(s)
Les matheux et Schrödinger ont reconnu cette équation immédiatement (?) : elle conduit à la fonction hypergéométrique dégénérée de Kummer, qui conduit aux [[polynômes de Laguerre]], '''ssi''' n-l-1 est un '''entier positif''' : donc '''n est un entier positif''' et l = 0, 1 , 2 , .. ,n-1 . Et le nombre k est simplement k = n+l-1.
*Pour le cas l= n-1 (les états de Rydberg (cf. [[atome de Rydberg]]), elle devient r .g " + (2n-r) g' = 0 satisfaite par g = cste (en effet S(r) ne doit avoir aucun nœud quand le nombre quantique radial k est nul !).
*Ici, on fera les calculs "à la main" pour les faibles valeurs de n .Mais sinon, les ''aficionados'' des équations différentielles chercheront un développement de f(s) en série entière qui se STOPPE en un polynôme P(s): cela marche, c'est le raisonnement typiquement utilisé avec l'équation hypergéométrique !
=== Infeld-Hull et la "factorisation" ===
dans RevModPhys 23,1951,21-68 , on constate que la méthode des opérateurs d'échelle était bien connue à l'époque (cf aussi Durand, CRAS1950,230,273):
L'idée est classique :
soit A = 1/2 -a/r -d/dr et B = 1/2 - b/r +d/dr en unités "bien choisies".
A et B sont opérateurs sur les fonctions de carrés sommables sur [0, infty[.
Ils sont opérateurs conjugués pour a = b .
et l'équation de Leibniz-S s'écrit :
A(l+1)B(l+1) Snl = (n-l-1)Snl/r
En multipliant par Snl et en sommant il apparaît immédiatement que n-1> l ;
et B S = 0 pour l = n-1 d'où la valeur de S "circulaire" :
S(r) = r^n .exp (-r/2)
Qq calculs permettent de trouver que
S(n+1, l) = r A(n) S(n,l)
S(n-1,l) = rB(n) S(n,l) .1/[(n-1-l)n+l)]
et toutes sortes de relations sur les polynômes de Laguerre.
Noter aussi que l'équation du second ordre peut s'écrire , comme assez souvent :
K(n,l) S(n, l-1) = A S(n,l)
K(n,l) S(n,l) = B S(n, l-1) (Durand p 449)
*Les relations de Pasternak permettant de calculer <r^k > =((n, l,k)) s'en déduisent :
k+1)<r^k> -2n(2k+1)<r^(k-1)> +[(2l+1)²-k²]<r^(k-2)> = 0
*exemples classiques
*(n,l,3) = n²/8[ 35 n^4 -35 n² -30 n²(l+2)(l-1)-3(l+2)(l+1)l(l-1)]
*(n,l,4) = n^4/8[63 n^4 -35n²(2l²+2l-3)+5l(l+1)(3l²+3l -10) +12]
*(n,l,-1) = viriel = 1/n²
*(n,l,-2) = force = 1/n^3(l+1/2)
*(n,l,-3) = force de barrière et LS = 1/n^3(l+1/2)l(l+1)
*(n,l,-4) = ion-dipôle => cf Kondratiev = [3n²-l(l+1)]/2n^5(l-1/2)l(l+1)(l+1/2)(l+3/2)
*noter l=0 pour -3 et -4 ! il faudra être prudent avec les électrons s !
*(n,l,2) = n²(5n²+1-3l(l+1))/2
*(n,l,1) = 3n²-l(l+1)]/2
Certaines se trouvent dans [[atome d'hydrogène]]
== [[Invariant de Runge Lenz]], quantique ==
=== Champ coulombien ===
*Le cas de la force coulombienne (cf. [[mouvement keplerien]] ; le [[puits de potentiel]] a déjà été étudié en mécanique classique) est TRÈS PARTICULIER car il montre que n DOIT être un '''entier positif''', '''indépendant de l''' , alors que les fonctions propres g(n,l,r) dépendent bien de deux indices n et l :
les valeurs propres de l'énergie ne dépendent pas séparément de n et de l , mais '''seulement de n''' , entier positif, qui de ce fait est appelé nombre quantique principal de couche (avec n= 1 -> couche K , n=2 -> couche L ,..).
Ce fait, très exceptionnel pour l'énergie, ne sera plus vrai pour un potentiel V(r) quelconque, même voisin de -e²/r. Il convient donc de ne pas trop s'y attacher, sauf si l'on veut s'expliquer cette dégénérescence (anciennement appelée dégénérescence accidentelle), via le raisonnement de Pauli.
=== vecteur excentricité quantique ===
Le vecteur excentricité (cf. [[mouvement keplerien]] et [[invariant de Runge Lenz]])vaut :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = \vec{V} \wedge \vec {L} / (GMm) -\vec{r}/r </math>
|}
|
| |
|}</div>
Il existe aussi en mécanique quantique, en tant qu'opérateur observable. Il vaut en unités convenables (unités atomiques)
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \hat{\vec e} = [\hat {\vec {p}} \wedge \hat {\vec {L}}-\hat {\vec {L}} \wedge \hat {\vec {p}}]/2- \hat {\vec {r}}/r</math>
|}
|
| |
|}</div>
Or rappelons qu'en termes d'opérateur:
'''p^L +L^p''' = 2i.'''p'''.<math>\hbar</math>
ce qui rend légèrement différent le vecteur quantique , subtilité de l'algèbre non commutative !
=== propriétés de la Q-excentricité ===
Toujours en faisant les calculs d'opérateurs,
on retrouve e.L = 0 , L.e = 0 , e.H = H.e (donc e est bon nombre quantique , et donc dans un sous-ev de la valeur propre de H , e sera stable).
<div style="text-align: center;"><math>e^2 -1 = -(H/E_o)\cdot [L^2/\hbar^2 + 1]</math></div>
Là encore un terme (+1) vient subrepticement se glisser dans les calculs (on a pris Eo = -13.6eV):
Et [e^2,Lz]=0
Mais alors ,dans l'ECOC [H, L², Lz], e² est un bon nombre quantique, et sa valeur est, dans le niveau n :
<div style="text-align: center;">e² = 1 -1/n² -l(l+1)/n²</div>
et par conséquent l ne peut dépasser n-1 ;
Mais on n'attendait pas cette bizarre formule !
=== Boost et Q-excentricité ===
*Et maintenant, la RÉVÉLATION pour tous ceux qui ont fait de la relativité restreinte :
Multiplions le vecteur excentricité par \hbar pour lui donner l'unité d'un moment cinétique et par n par pure commodité dans les calculs.
Nous appellerons ce vecteur <math>\hat{\vec E}</math> ,le vecteur excentricité-boost , qui est un vecteur polaire et non axial.
E commute avec L² , mais pas avec Lz ; et E² est un bon nombre quantique dans l'ECOC [H,L²,Lz], '''mais pas E''' !
MAIS, dans le sous-ev de la couche n ,
<math>[F_{\lambda \mu},F_{\mu \nu}] = F_{\lambda \nu}</math>
où le tenseur antisymétrique 4-4, F est :
(0,E) en première ligne et la matrice 3-3 antisymétrique correspondant à L^ .
VOILA ! l'atome d'hydrogène est invariant par SO(4) [ évidemment pour les états d'énergie positive, par SO(3,1) c'est à dire le groupe de Lorentz ! d'où l'idée de la notation excentricité-boost ! ]: cela était connu de Pauli , de Fock , de Bargmann , etc. Mais à l'époque, peu connaissaient aussi bien que Pauli la relativité restreinte !
Pour démontrer ces relations, il vaut mieux avoir qq notions d'algèbre de Lie (et des formules de trigonométrie correspondantes), car sinon cela peut être un peu long (11 pages dans le X ; et une page dans le Y : X et Y par courtoisie).
=== opérateurs S et D, valeurs propres de H ===
Il "suffit" maintenant de se rendre compte que [H, Lz, Ez] forme un ECOC ( ce qui correspond en mécanique classique aux coordonnées paraboliques et à la vision spinorielle :
soit 2S = L + E et 2D = L- E ;
Alors S² - D² = 0
S et D sont deux moments cinétiques de carrés égaux : s(s+1)
et :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> [S^2 + D^2 + \hbar^2]\hat{H} = E_o \cdot \hbar^2</math>
|}
|
| |
|}</div>
C'EST FINI : H a pour valeurs propres : E_o/n² avec 4s(s+1) +1 = n²
soit n = s+s+1 , donc de dégénérescence : n² (faire ce petit calcul !).
Voici comment depuis 1926, on eût pu enseigner l'atome d'hydrogène de Pauli (Nobel en 1945 après Heisenberg, Schrödinger et Dirac en 1933).
Pourquoi cela ne s'est-il pas produit ? Vraisemblablement parce que les orbitales paraboliques étaient moins utiles que les orbitales- harmoniques sphériques qui privilégiaient donc l'ecoc [H,L²,Lz].
<!--
== Voir aussi ==
* [[atome]]
* [[atome d'hydrogène]]
* [[Théorie de Schrödinger de l'atome d'hydrogène]]
* [[atome à N électrons]]
* [[Classification périodique]]
----
-->
== Compléments sur SO(6)et SO(4,2) ==
vacances closes après avoir vendu mes merguez, je fais le point sur SO(6). Cf Oliver.
SO(6) comporte <math>C_6^2</math> = 15 générateurs de rotation.
(P. Kustaanheimo and E. Stiefel, J. Reine Angew. Math. 218, 204 (1965). )
la transformation K-S amène l'eq de Schrodinger sous une forme simple :
multiplions tout par r :
<math>-1/2 r \Delta +1 = E\cdot r</math>
et opérons le changement de variables ; il vient :
<math>[L_{56}+ L_{46}-2E \cdot (L_{56}- L_{46})-1]|\psi>=0</math>
En utilisant le "tilt" usuel A , tel que -2E = exp2A et les relations de commutation avec L(45) , l'équation se réécrit :
<math>[e^{i A L_{45}}L_{56}e^{-i A L_{45}}-e^{-A}]|\psi>=0</math>
La solution est immédiate :
les vecteurs propres de L(56) sont <math> \ |\phi_n></math> de valeur propres n= 1,2,3,... et donc A = - ln n et on en tire
<math> \ E_n = -1/2n^2</math> ,
puis en opérant la transformation réciproque de K-S , on retrouve les états propres paraboliques <math> \ |n_1,n_2,m></math>, puis via les symboles 3j-de-Wigner , les états sphériques <math>\ |n,l,m></math> (Kleinert p 964):
Que tout cela paraît naïvement facile! Néanmoins rappelons que Feynman avait calé sur ce problème et que le déblocage de situation s'effectua de 1967 à 1998.
== Retour ==
*[[Mécanique, enseignée via l'Histoire des Sciences|Mécanique , enseignée via l'Histoire des Sciences]]
[[Catégorie:Mécanique, enseignée via l'Histoire des Sciences (livre)]]
a2i9vev0q8ogljvptsf99yz1pmq464c
763186
763127
2026-04-07T17:05:17Z
Peyraut
123445
/* Vecteur excentricité, e o → {\disp */
763186
wikitext
text/x-wiki
<noinclude>{{Mécanique, enseignée via l'Histoire des Sciences}}</noinclude>
Il s'agit du mouvement d'un point dans un champ central '''F'''('''OM''') = - GMm. '''OM'''/OM³, dit Newtonien.
Kepler en a énoncé les 3 lois principales :
*La planète P a pour trajectoire une ellipse dont le soleil O est un foyer.
*Le rayon vecteur '''OP''' balaye des surfaces égales dans des temps égaux.
*Le carré de la période T du mouvement est comme le cube du grand axe, 2a, de l'ellipse.
La démonstration de ces faits revient à Newton (1684).
L'article mouvement keplerien de la Wiki a été beaucoup modifié.
Nous en rapatrions l'essentiel.
== Le mouvement est central ==
les conséquences immédiates sont :
* Le moment cinétique '''L''' est une constante L<sub>0</sub>. On pose '''L''' = m.'''C''')
* Donc la trajectoire est plane, perpendiculaire en O à L<sub>0</sub>
* Dans ce plan , le mouvement tourne autour de O ('''toujours dans le même sens''', choisi comme positif).
* La loi des aires de Kepler est satisfaite : dS/dt = C/2 = 1/2 r².d<math>\theta</math>/dt.
* Comme C est non nul, thêta est une échelle de temps (non linéaire) mais souvent utilisée(cf Note).
* L'hodographe et la trajectoire sont en '''correspondance directe''' : l'un donne l'autre. L'espace des phases sera donc bien R<sup>2</sup> x R<sup>2</sup> , mais de manière très simplifiée.
Note-annexe : historiquement, Ptolémée a utilisé theta' = MF'O = ~ t (+ O(t^3)), car cela suffisait pour les observations de l'époque : cela s'appelle la théorie de l'équant, elle sera vue en exercice.
Note 2 : on a excepté le cas L=0 comme physiquement irréalisable : on doit toujours pouvoir s'y ramener à la limite, et c'est un joli-exercice.
== L'hodographe est un cercle ; donc la trajectoire est une ellipse ==
==='''l'hodographe est un cercle :'''===
Poser p = C<sub>0</sub>²/GM (on verra que c'est la longueur du semi-latus-rectum (on dit aussi "paramètre" de l'ellipse), et V<sub>0</sub> = C<sub>0</sub>/p (qui est donc une vitesse, par ailleurs pseudo-scalaire). Alors, on trouve :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{V} = V_0 \times\vec{n}+ \vec{V_1} </math>
|}
|
| |
|}</div>
multiplier par vecteur(k).wedge et diviser par Vo ; on obtient :
=== la trajectoire est l'ellipse : ===
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e}\cdot \vec{r} = p-r </math>
|}
|
| |
|}</div>
===Démonstration :===
prendre comme échelle de temps theta(t) ; le Principe Fondamental de la Dynamique de Translation (PFDT) donne :
<div style="text-align: center;">
<math> \frac{d\vec{V}} {d\theta} = - V_0 \cdot \vec{u} </math>.</div>
donc, par intégration sur la variable theta avec le vecteur unitaire <math>\vec{n}</math> perpendiculaire à <math>\vec{u}</math> :
<div style="text-align: center;"><math>\vec{V} = V_0 \times \vec{n} + \vec{cste}</math>.</div>
soit :
<div style="text-align: center;"><math> \vec{V} = V_0 \times \vec{n} + \vec{V_1}</math>.</div>
Il y a évidemment beaucoup de manière de retrouver le vecteur constant "cste = V1" , en prenant deux valeurs de la vitesse remarquables ; par exemple, la vitesse à l'apogée et au périgée donnent: V(A) = V<sub>0</sub> + V<sub>1</sub> et V(A') = V<sub>0</sub> - V<sub>1</sub> avec V<sub>1</sub> = e×V<sub>0</sub>.
'''''nota bene''''' :''Et Voilà ! C'est fini'' ! L'hodographe est bien un cercle ( de rayon V<sub>0</sub> = C<sub>0</sub>/p) ! La trajectoire sera donc FERMEE ! On obtient donc cette caractéristique FONDAMENTALE du mouvement dès le début du raisonnement. Cette simple remarque a été faite en 1713, mais est passée relativement inaperçue. Il en est résulté des dizaines de re-découvertes ! Jusqu'en 2000, on peut voir des articles ( cf par exemple Butikov, etc.)signalant cette "trouvaille". On peut s'amuser à exploiter cet hodographe, sans doute comme l'a fait Hooke ( tentative dite des elliptoïdes ; rappelons que Hooke n'avait pas grande culture mathématique, mais il avait compris le principe de l'hodographe, puisque c'est cette méthode de l'hodographe qu'il utilise pour l'ellipse dite de Hooke).
==== '''Vecteur excentricité''', {{MathText|\vec{e_o}|eo}}, constant ====
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = ( \vec{V} \wedge \vec{z})/ V_0-\vec{u} </math>
|}
|
| |
|}</div>
C'est l'extraordinaire intégrale première de Hermann(1713)- retrouvée par Laplace-Runge-Lenz,etc.! Il en sera question plus tard.
La démonstration est immédiate : multiplier l'équation de l'hodographe par r et faire le produit scalaire avec <math>\vec{u}</math>, et la réécrire.
Il faut préciser que <math>\vec{z}</math> est le vecteur unitaire perpendiculaire au plan. On pourrait faire autrement sans ce produit vectoriel mais alors il faudrait ensuite faire le produit scalaire avec la normale et non avec le vecteur rayon, puis multiplier par r.
===='''donc la trajectoire est une ellipse :'''====
Car en multipliant scalairement le vecteur-excentricité <math>\vec{e}</math> par le rayon-vecteur <math>\vec{r}=r\vec{u}</math>, on obtient pour le produit vectoriel :
<math>(\vec{V}\wedge\vec{z}).\vec{r}=rV\sin\alpha=r^2w=C</math>, d'où :
<math>e \cdot r cos\theta = C/V_0-r=p-r</math> , soit :
<div style="text-align: center;">
{| border="0"
|-----
|
{| border="1" cellpadding="10" style="border-collapse:collapse"
|-----
| <math> r = \frac{p}{1 + e \cos \theta} </math>
|}
|
| |
|}</div>
Ce qui est l'équation polaire d'une ellipse d'excentricité e , et de paramètre p , avec le vecteur-excentricité sur l'axe des apsides et la convention '''origine des angles au périgée'''. La valeur de p ( demi-latus rectum := b^2/a := a(1-e^2)) est :
<div style="text-align: center;">
{| border="0"
|-----
|
{| border="1" cellpadding="10" style="border-collapse:collapse"
|-----
| <math> p = \frac{C_o^2}{(GM)}</math>
|}
|
| |
|}</div>
Évidemment, on peut prendre l'autre convention, '''origine des angles à l'apogée''' ; soit <math> r = \frac{p}{1 - e \cos \theta}</math>,
==='''La conservation de l'énergie''' ===
si l'on introduit l'énergie potentielle -GMm/r , elle conduit à :
1/2 V² - (GM)/r = Eo/m = cste , d'où
<div style="text-align: center;">'''Eo négative == - GMm/2a.'''</div>
'''Exercice''' : montrer que 2a est le grand-axe de l'ellipse.
Donc dans le plan de la trajectoire, les deux quantités physiques '''Lo''' et Eo déterminent la forme de l'ellipse. Bien sûr '''OMo''' et '''Vo''' aussi.
==='''moyens mnémotechniques''' par @d ===
il importe, dans les exercices, de ne pas toujours tout redémontrer, et de savoir retenir les formules encadrées : la méthode d'A.D., dite des d@hus, sert en ce genre de situation :
les seuls paramètres sont cinématiques : GM (constante de Gauss) , Eo/m (énergie massique), et Co (constante des aires).
Donc, un de trop !
'''MAIS''' il suffit de retenir
*p = @d[GM, Co] ( et pas de Eo) ; et de retrouver la constante par le cas particulier du cercle (donc constante = 1)
*2a= @d[GM, Eo/m] ( et pas de Co) ; et de retrouver la constante par le cas particulier du cercle (donc constante = -1)
=== remarque de Hooke-Hamilton ===
Signalons à titre de curiosité ce raisonnement de Hooke, qui a peut-être des résurgences dans la pensée de Allais (Nobel économie quand même !) :
Si l'on considère que le mouvement est plan central, de centre O , pourquoi ne pas dire que la force est centrale et proportionnelle à l'angle balayé par unité de temps, soit <math>\dot{\theta}</math> , alors on retrouve tous les résultats antérieurs. Il est fort possible que ce soit par cette méthode que Hooke ait essayé de retrouver "la fameuse loi en 1/r²" , en appliquant sa méthode du second ordre : se donner la position initiale, puis la position voisine. Alors appliquer la loi et trouver la position ultérieure. Itérer. Il trouva par cette méthode des "elliptoides", ce que méprisa Newton. Plus fin, mais quel mérite en 1820? , Hamilton tirera de cette loi le fait que l'hodographe est un cercle, et tout le reste s'ensuit comme on l'a vu.
Ainsi les lois de Newton seraient simplement liées à un <math>\dot{\theta}</math>. Cette méthode serait plus "économique". Par contre, elle induirait peut-être un malaise, si on l'interprète à la manière Allais, car alors l'interposition de la Lune entre Soleil et Terre pourrait modifier l'angle sous lequel le Soleil serait vu de la Terre, et ainsi modifier "G" : une telle manière de faire serait alors en contradiction avec l'astronomie des trois corps. Il faudrait aussi retrouver la gravimétrie et les "théorèmes remarquables de newton-gauss". Dans cette problématique, on serait alors entraînés fort loin...Cela est bien curieux et ne vaut que pour l'anecdote : il est sain d'avoir toujours des visions différentes ( mais si elles débouchent...sur quelque chose de tangible).
== Mouvement sur la trajectoire ==
* La loi des aires donne S/T = Pi.a.b/T = Co/2 , ce qui donne :
{{exemple||loi de Kepler(1628)|<math>\omega^2 \cdot a^3 = (GM) </math>}}
* Partant du périhélie, et en introduisant l'angle dit [[anomalie excentrique]] E(t)(cf dessin), géométriquement :
<math>tan \theta/2 = tan E/2 \cdot \sqrt \frac{1+e}{1-e}</math>
<math> r = a (1- e \cdot \cos E)</math>;
On calcule géométriquement l'aire balayée depuis le passage au péricentre :
par affinité , S(t) = (b/a)[a²E/2 -ac. sinE /2] = ba(E-e.sinE)/2
Il s'ensuit :
{{exemple||Équation du temps de Kepler|<math>\omega t = E - e \cdot \sin E </math>}}
La fonction réciproque donne E(t), et de là '''OM'''(t).
----
===Fin du Cours===
Il est évident que l'on a cherché ici la compaction maximum du cours.Des dizaines d'ouvrages reprennent ce problème.
Pour nous, 2 ressortent du lot : Chandrasekhar si on aime la géométrie . Tisserand ou Winter si on veut plus exhaustif.Quelques exercices classiques suivent, pour "se faire la main".
----
==Exercices ==
Il y a des dizaines d'exercices sur ce sujet, évidemment très important; soit de satellites artificiels, soit d'astronomie. Nous "essaierons" de les classer.
=== satellites de la Terre ===
'''exMersenne-Descartes-Laplace :'''
Mersenne posa à Descartes la question suivante : si on tire un boulet verticalement, est-il possible que le boulet ne redescende pas?
Soit h = Vo²/2g . Montrer que l'altitude H atteinte est :
1/H = 1/h-1/R . que se passe-t-il pour h > R .
Que penser du cas Vo<c et c²< 2gR (Laplace vers 1800).
----
'''Système d'unités :''' pour la Terre , nous éviterons GM remplacé par gR² avec profit. Du fait de La loi de Galilée, la masse du satellite m n'intervient jamais. On se retrouve donc avec un système d'unités adapté ( un d@hu) tronqué à la cinématique.
*R étant l'unité de longueur, on prendra 2π.R = 40 000 km.
*On conviendra de prendre g = 9,80 m/s².
*La pulsation unitaire sera donc <math>w = \sqrt{\frac{g}{R}}</math>, dite pulsation de Schuler. Il lui correspond une '''période''' <math>T(R)= 2 \pi \sqrt{\frac{R}{g}}</math>, dite période basse altitude (84,4 min).
*La vitesse unitaire est <math>Vo = wR = \sqrt{gR}</math>= 1re vitesse cosmique = 8.2 km/s (vitesse d'un satellite basse altitude).
*L'énergie massique du satellite est donc -1/2 .gR
*Le pivotement sidéral de la Terre est 24h * (365.25/366.25) = 86164 s =17.0 To.En un jour les astronautes voient donc environ 18 fois le Soleil se lever.
En pratique, les satellites d'observation , type Spot orbitent à ~ 800 km d'altitude.
Reprendre le système d'unités de ces satellites.
----
'''Légère erreur de trajectoire :'''
Au lieu de la bonne vitesse Vo de Spot, on donne une vitesse de bonne direction (i.e perpendiculaire au rayon) mais trop forte : V1 = Vo(1+eps). Trouver la trajectoire et la période.
- - - - -
'''Fenêtre de tir :'''
m ex que le précédent mais la bonne vitesse Vo est mal orientée dans le plan d'un angle A , petit. Trouver le périgée.
- - - - -
'''Erreur radiale :'''
m ex que le précédent, mais il y a en sus de Vo , une erreur de vitesse radiale Vo.eps.
----
'''Lâcher-Chute libre :'''
On n' a pas attendu Newton (le 24 Nov 1679) pour réfléchir à la déviation vers l'Est (ou l'ouest!) d'une pierre lâchée de l'équateur; c'était la dispute favorite des Coperniciens et anti-Coperniciens. La vitesse due au pivotement est à l'équateur de 40 000 km/86164 s soit 464 m/s . Selon les anti-Coperniciens, une chute de 5m (environ 1s) eût placé le mobile vers l'Ouest de 464 m ! Galilée (mais il avait tort) disait que le corps tomberait toujours à la verticale. Koyré catalogue les différents types de solutions (chute des graves et mouvement de la Terre): l'imagination au pouvoir ! mais c'est Newton qui donna la solution.
Soit h << R , retrouver le résultat de Newton.
Si h est assez grand, la déviation vers l'est sera si grande que la pierre sera satellite.
Si h = altitude géostationnaire = H , la pierre ne tombe plus !
Si h est encore plus grand , la pierre est à son périgée : elle remonte, périodiquement.
Si h > (R+H) .2^(1/3) - R , qu'arrive-t-il ?
----
'''Balistique :''' voir la WP ( [[ellipse de sûreté]] )
revoir la leçon sur la chute libre avec violence (avec vitesse initiale dit-on aujourd'hui).
Dès que l'on veut une certaine précision (théorique) , il faut tenir compte de ce que la Terre est sphérique et donc prendre comme trajectoire de l'obus une ellipse lancé d'une base B avec une vitesse Vo faisant l'angle A avec la verticale. Soit u = Vo/sqrt(gR).
1/. Relation u et A pour que l'obus tombe à l'antipode.
2/. Déterminer la portée 2R.Beta , via tan B = f(u, tan A).
3/. Pour B donné, combien y a-t-il de trajectoires possibles ? et quelle est la portée maximale.
(Indication : soit H le point d'altitude maximale (pour A=0 !). La trajectoire a pour deuxième foyer un point situé sur le cercle [centre B ; rayon BH]).
----
=== Corrigé des exercices ===
'''Mersenne-Descartes :'''
Appliquer le théorème de l’Énergie cinétique :
-gR²/r +1/2 V² = constante , ce qui conduit au résultat.
Descartes évidemment ne savait rien de tout cela ; mais il se doutait "intuitivement" que si g(z) décroissait alors il y aurait possiblement une "vitesse de libération".
De même , Laplace , très heuristiquement , remarqua que si aucun corps ne pouvait dépasser la vitesse-limite c , alors si c² < 2gR , l'astre serait un trou noir !
Enfin, l'expérience a été tentée ( plus pour tester la relativité galiléenne et/ou la déviation vers l'Est(cf exo plus loin)): bien sûr on n'a jamais retrouvé le boulet! )
----
'''Système d'unités Spot :'''
Ro = 40 000/2Pi +800 = 7166 km.
To via Kepler est : 84.4 (7166/6366)^3/2 = 100 min.
Tout le reste s'en déduit (attention , c'est la pulsation qui a été choisie unitaire).
----
'''Légère erreur de trajectoire :'''
Si eps = sqrt(2) -1 , la trajectoire est parabolique et le satellite part à l'infini.
Sinon , Mo est le périgée: a-c = Ro. D'autre part, E1/m = 1/2 V1² - gR²/Ro ; donc on obtient le grand axe , puis l'apogée en A1 : OA1 = Ro.(V1²/2Vo²-V1²) (On retrouve le cas V1 = Vo.sqrt(2)).
Si eps est petit : l'énergie massique a peu varié : dE/m = mVo².eps . Puis dE/Eo = - da/Ro = -2/3 . dT/To . Donc OA1 = 4Ro.eps et l'excentricité est e = 2eps ; enfin dT = To.3eps
- - - - -
'''Fenêtre de tir :'''
Cette fois, l’Énergie massique n'a pas changé, donc le grand axe vaut 2Ro . Comme OMo = Ro , c'est l'extrémité du petit axe. donc k/\OMo donne la direction du grand axe. La projection de Mo sur celui-ci donne le centre de l'ellipse : l'excentricité vaut donc e = sin A ; d'où le périgée OP1 = Ro(1-sinA) : on ne peut se tromper que de 100 km :cela donne une fenêtre sin A = 100/7166 rad = 0.8°. Assez large , car les pointeurs donnent la seconde d'arc.
- - - - -
'''Erreur radiale :'''
Si eps = 1 , la trajectoire est parabolique !
Cette fois, le moment cinétique Lo est le bon ; donc le paramètre p est le bon . Donc OMo est perpendiculaire au grand axe , dont la direction est connue. Il est facile de calculer le vecteur excentricité qui donne en module eps.
On en déduit a = Ro/(1-eps²) (on retrouve eps = 1 comme limite).
----
'''Lâcher-Chute libre :'''
le Cours donne D = déviation vers l' Est de 2/3.wt.h .
Démontrons-le , façon Newton : la trajectoire est une ellipse , mais où r varie sensiblement comme R+h-1/2gt². La conservation du moment cinétique donne :
d<math>\theta</math>/dt = [(R+h)/R+h-z)]² .w = w (1+ 2z/R),
soit une déviation w.R. int(2z/R) = 1/3 w.gt².t = 2/3 wt.h
Si h= H , c'est l'exercice classique du géostationnaire :
R+H = R .17^(2/3) = 6.6 R = 42 000 km
Si h < H , il existe des trajectoires elliptiques dont Mo est l'apogée : la plus petite aura pour périgée OP = R , donc un grand axe 2a = 2R+H , d'où l'énergie massique . En posant r = Rx , on trouve x^4 + x^3 = 1/2 (289) , soit x = 4.67 et donc h = 3.67 R.
Si h > H , la pierre remonte ! résultat curieux qui aurait sans doute amusé Mersenne, et elle part à l'Ouest (si l'on ose dire).
enfin si h > H. 2^(1/3)= 8.36 R, alors E > 0 , donc trajectoire hyperbolique (limite : parabolique).
----
'''Balistique :'''
V= 8.2km/s := sqrt(gR) a signé le début de la Guerre Froide.
mais déjà les canons longue portée obligeaient à prendre une trajectoire elliptique et non parabolique : 111.111 km c'est déjà 1° à l'équateur!
1/. Si l'obus arrive à l'antipode B' , OB = OB' = paramètre p = Lo²/m²gR² = R soit u.sinA = 1 . (évidemment trajectoire avec A< 45° : il faut une apogée!)
2/. La portée s'évalue en calculant la direction du vecteur-excentricité 1 + i.Lo.Vo.exp(iA)/mgR² = [1-u²sin²A] +i[u²sinAcosA]=> tanB = 1/2 u².sin2A / (1-u²sin²A).
Pertinence : on retrouve Torricelli pour u <<1 ; et le §1.
3/. Pour B donné , équation en tan A :
tan²A (1-u²) - tan A (u²/tanB) + 1 = 0 d'où deux angles B1 et B2 tels que tan(B1+B2) = (tanB1+tanB2)/(1-tanB1.tanB2) = S/(1-P) = -1/tanB, donc A1+A2 = Pi/2+ B : il existe une trajectoire tendue et une plongeante. Portée maximale : tan B = u²/2(1-u²) [pertinent avec u=1 ]
'''Géométriquement''', tout ceci est relatif à la courbe de sûreté qui est l'ellipse de foyers T et B et d'apogée BH (rappel 1/H = 1/h -1/R , exercice sur l’énergie potentielle). En effet , toutes les trajectoires Tr(A) ont m énergie , donc m grand axe , soit TH+HB . Le lieu du deuxième foyer est donc le cercle [centre B, rayon BH]: pour une portée donnée (donc angle B donné , il y a deux solutions : à l'intersection de la droite d'apogée avec ce cercle ; soient F1 et F2 : alors la vitesse initiale étant bissectrice de TBF , les deux vitesses sont telles que A1+A2 = Pi/2+ B. La racine double est lorsque sinB = H/R ( = u²/(2-u²)).
L'ellipse de sûreté est donc telle que MT+MB = HO+HB, et dans ce cas, BM est corde focale [les raisonnements sont calqués sur ceux de Torricelli].
----
=== Exercices d'astronomie ===
==== Étoiles doubles ====
Montrer que dans le cas d'une étoile double, la troisième loi de Kepler s'écrit assez naturellement :
w² . a³ = G (m1+m2)
Que penser des planètes du soleil ?
'''Réponse :'''
Le problème à deux corps donne la réponse : (masse-réduite).w² a = G.m1.m2/a². Ainsi , on obtient une formule symétrique en m1 et m2 , ce qui est pertinent.
Dans le cas des planètes du Soleil , la plus grosse, Jupiter, n'apporte qu'une petite correction m2<< M(Soleil) , ce qui justifie la loi de Kepler. Pour les calculs précis, on fait les corrections, étant entendu que le barycentre du système solaire est quasiment en mouvement uniforme (pour plus de corrections, par exemple pour la ceinture de Kuiper ou le nuage de Oort, il faut envisager la "marée galactique").
==== Conjonction Mars -Terre ====
La distance T-Soleil = 1UA ,période 1an, excentricité e(T); mars-Soleil = d UA,période k ans, excentricité e(M). Montrer que '''TM''': = '''OD''' ne peut varier que dans une couronne >d1 et <d2. Le point D est-il dense dans la couronne? Si k était rationnel := p/q quel serait le mouvement de D.
'''Réponse :'''
Consulter exercices de l'IMCCE.
==== équant de Ptolémée ====
Soit une planète, disons Mars, de trajectoire elliptique d'excentricité e ( = 0.093).
On prend comme échelle de temps l'angle polaire compté à partir du deuxième Foyer F', où "il n'y a rien!".
Est-ce mieux ou moins bien que de compter theta(t) comme temps "uniforme" ?
'''Réponse :'''
Historiquement, cet exercice a beaucoup d'importance : on ne distingue pas un cercle d'une ellipse dès que e<0.1.
Donc Ptolémée croyait que la trajectoire était circulaire. MAIS il avait bien vu que theta(t) n'était pas uniforme ; par contre theta ' (t) l'était à la précision des mesures de l'époque. C'est ce que l'on demande de prouver.
----
== Rapatriement provisoire de la WP:historique de démonstrations ==
Ici est placé tout le travail de recherche historique qui n'intéresse pas forcément tout le monde : il y eût moult "démonstrations" du cours précédent.
=== Newton (1684) ===
*1/. '''la première''', celle de Newton en novembre 1684, est géométrique, le temps étant évalué par l'aire balayée (2ème loi de Kepler) : l'analyse en est faite dans l'[[Exégèse des Principia]].
=== Hermann (1710) ===
*2/. '''la plus simple''' (1710 & 1713) est celle de [[Jakob Hermann]] (1678-1733), élève de [[Jacques Bernoulli]] (1654-1705) : il écrit à [[Jean Bernoulli]] (1667-1748) : on remarque que l'hodographe est un cercle (notion de vecteur excentricité) : en calculant le produit scalaire '''e.r''', on trouve l'ellipse et son péricentre. L'analyse est faite dans [[Invariant de Runge Lenz]].
Laplace la reprendra dans son traité de « Mécanique Céleste ».
Que cela est vite dit dans notre langage moderne ! En réalité, la démonstration géométrique est la remarque classique sur le rôle des podaires dans le cas de champs centraux. Danjon remarque (avec Hamilton) que l'anti-podaire de l'inverse d'un cercle est une conique : cela était enseigné encore au baccalauréat des années 60 (Cf. LEBOSSÉ & EMERY, cours de mathématiques élémentaires).
Quant à Hermann, c'est un tour de force :
Il possède trois intégrales premières en coordonnées cartésiennes tirées de <math>\ddot{x} = -gR^2\cdot x \cdot r^{-3}</math> et idem en y.
* <math>C := x\dot{y} -y\dot{x}</math>
* <math>E_x := \frac{x}{r} - \frac{C}{gR^2}\cdot \dot{y}</math>
* <math>E_y := \frac{y}{r} - \frac{C}{gR^2}\cdot \dot{x}</math>
Éliminer la vitesse : on trouve <math> x \cdot E_x +y\cdot E_y = r- p </math> : c'est une ellipse (Cf.discussion [[conique]], Kepler).
Mais comment a-t-il trouvé les deux intégrales premières du vecteur excentricité ? par un raisonnement analytico-géométrique horriblement compliqué ! On sait aujourd'hui le faire par la théorie de la représentation linéaire des groupes (Moser et SO(4) :1968)
=== Transmutation de la force par Newton ===
*3/. '''la plus surprenante''' est celle de la [[Transmutation de la force]] (Newton, retrouvé par Goursat (1889)): ce théorème est EXTRAORDINAIRE et apprécié des ''afficionados'' des ''Principia''.
=== Keill (1708) ===
* 4/. '''la classique''' : Newton-Keill (en 1708) - Bernoulli (1719)
"Classique", elle est bien "chencitournée".
Le problème est plan, si la force est centrale. Le plan de phase est donc (<math> x,y,\dot{x} , \dot{y}</math>). Les deux équations du PFD (principe fondamental de la dynamique) sont :
<math>\ddot{x} = - \Omega^2 \cdot x</math>
et la même en y.[Évidemment <math>\Omega </math> dépend de r!].
Cette notation est évidemment très réminiscente de celle de Hooke. Mais elle n'a rien à voir, sinon que la symétrie est centrale.
Choisir trois fonctions invariantes par rotation :
*<math>I := 1 \cdot (x^2+ y^2) = r^2</math>, strictement positif,
*<math>J := 1 \cdot (x\dot{x} + y\dot{y})</math>, de sorte que <math> \dot I = 2J (= 2r\dot{r})</math>,
*<math> \ K := {v^2}/{2}</math>, énergie cinétique.
Remarquer cette particularité : r² est choisie comme variable, et non r. Et comme J est non-nulle, I va jouer '''le rôle d'une échelle de temps''' au moins sur une demi-période, du périgée à l'apogée.
Démontrer que le problème se réduit au système différentiel (S) :
*<math>\dot{I} = 2J</math>
*<math>2\dot{J} = K -I \Omega^2(I)/2</math> (th du viriel !)
*<math>\dot{K} = - J \Omega^2(I)</math> (loi de Newton!)
- - -
Keill utilise alors '''l'échelle de temps I''' ; le système se réduit à :
*<math> 4\frac{d(J^2)}{dI} = 2K - I \Omega^2</math>
*<math>\frac {dK}{dI} = - \Omega^2/2</math>
En éliminant Omega² (et quelle que soit sa valeur ! donc c'est vrai pour toute force centrale!)
<math> K = \frac{J^2}{2I} + \frac{C_o^2}{2I}</math>.
C'est un vrai ''tour de force'' : au début du XVIII{{ème}} , on vient de réécrire :
<math>2KI = v^2\cdot r^2 = [\vec r \cdot \vec v]^2 + [\vec r \wedge \vec v]^2 = [\vec r \cdot \vec v]^2 +C_o^2</math>
Emmy Noether connaissait-elle cette démonstration due à l'invariance par rotation ?
- - -
Puis, l'invariance temporelle donne la conservation de l'énergie :
<math>1.\cdot H = K + V(I)</math>, où V(I) est l'énergie potentielle relative à la force centrale (= <math>-\frac{1}{2}\int \Omega^2 dI)</math>
- - -
Ces deux ensembles de surfaces feuillettent l'espace (I,J,K) et leur intersection donne l'orbite du mouvement dans cet espace.
Éliminer K conduit à travailler dans le demi-plan (<math>I, 2J = \dot{I}</math>), c'est à dire dans un plan de phase presque usuel (on joue avec r² plutôt qu'avec r) :
<math> H = \frac{J^2}{2I} + \frac{C_o^2}{2I} + V(I)</math>,
ce qui est '''l'équation de Leibniz(1689)''', mais en notation I = r². (Remarquer que tout résulte de cette circonstance (non évidente du temps où les vecteurs n'existaient pas) :
<math>x \dot{x} + y \dot{y} = r \dot{r}</math>)
et pour finir, comme d'habitude, dt = dI/2J donne le mouvement sur cette orbite de phase et la primitive de 2J(I) donne l'action S(I) du problème.
Évidemment, actuellement, nous repasserions immédiatement en coordonnées (r et r').
Il n'empêche que voilà décrite la solution incroyable de Keill qui témoigne d'une virtuosité tombée dans l'oubli de l'Histoire.
*'''Note d'histoire''':
cette équation ayant été écrite par Lagrange sous cette forme, le H ne saurait signifier « valeur de l'Hamiltonien » ! Peut-être faut-il y voir un hommage à Huygens (?), premier à utiliser la généralisation du théorème de l'énergie cinétique de Torricelli ? peut-être est-ce une simple notation fortuite...
La suite est très classique et correspond à différents paramétrages dans le cas de Kepler :
L'équation de Leibniz se réécrit dans ce cas :
<math> H \cdot 8r^2 -4C_o^2 + 8(GM) \cdot r = 4J^2 </math>
qui est une conique en J et r, ellipse si H est négatif de grand axe <math>2a = - \frac{(GM)}{H}</math> :
Il est usuel alors de paramétrer via l' »anomalie excentrique » :
<math>r = a \cdot(1- e \cos{\phi})</math>,
et « miraculeusement » :
<math>\omega \cdot dt = \frac{r}{a} \cdot d\phi</math> ,
qui s'intègre en donnant la fameuse équation de Kepler.
En contrepartie l'équation en theta est légèrement plus compliquée à intégrer (primitive de <math>\frac{1}{r}</math>) d'où :
<math>tan \frac{\theta}{2} = \sqrt{\frac{1+e}{1-e}} \cdot tan \frac{\phi}{2}</math>.
Note de détail: certains préfèrent la notation i = I/2 , et/ou j = J/2.
=== Clairaut (1741) ===
*5/. '''la méthode de Clairaut''' (1741), reprise par Binet consiste à écrire l'équation de Leibniz à l'aide de u := 1/r :
<math> \dot{r}^2 = 2H + 2gR^2 \cdot u - C^2 \cdot u^2</math>
et cette fois le paramétrage adéquat est :
<math>u := 1+e\cdot \cos \alpha</math> et <math> \dot{r}: = e\cdot \sin \alpha</math>
ce qui conduit au « miraculeux » <math>d \theta = d\alpha</math> ! la trajectoire est donc une ellipse.
Mais la deuxième intégration conduit à <math>dt = k d\alpha \cdot 1/u^2</math> plus difficile à intégrer (mais tout à fait faisable !)
=== Lagrange (1778) ===
*6/. '''la méthode de Lagrange''' est originale (1778) et n'utilise que la linéarité de F = m.a !
Partant de l'équation radiale de Leibniz(1689) :
<math>\ddot{r} = C^2 u^3 - e^2u^2</math>
il pose comme nouvelle variable z = C²-r et trouve :
<math>\ddot{z} = -(GM) \cdot z \cdot u^3 </math>,
'''identique''' aux deux équations de départ en x & y !!
donc il obtient : z (:= C²-r) & x & y linéairement liés, ce qui est la définition d'une ellipse (Cf. [[conique]], discussion). CQFD
=== Laplace (1798) ===
*7/. ''' Laplace''', sans citer Lagrange, calcule, en force brutale, sans aucune intégrale première, l'équation en I = x² + y² du troisième ordre issue du système de Keill : d'où il tire
<math>\frac{d^3I}{dt^3} = - \frac{\dot I}{I^{3/2}}</math>
(comme quoi , le jerk ne date pas d'hier!)
Laplace en tire cette fois '''quatre''' équations '''linéaires''' identiques :
d/dt(r^3.Z") = - Z', avec Z = r, x, y, constante.
D'où r = a x + by + c.constante : c'est une conique !
Il reste à trouver une interprétation physique à ce calcul!
=== Hamilton (1846) et autres ===
*8/. Soit une ellipse ; le foyer F et sa polaire, la directrice (D). Soit P le point courant de l'ellipse et PH sa projection sur la polaire. Le [[théorème de Newton-Hamilton]] donne immédiatement la force centrale F ~ r/PH^3 soit ~ 1/r².e³.
*9/. Hamilton démontre aussi que pour toute mouvement sur une ellipse de paramètre Po, on obtient |'''a/\v'''|.Po = C^3/r^3. Donc si le mouvement est central de foyer F, |a/\v| = a.C/r d'où a ~ 1/r².
*10/. Hamilton est aussi le promoteur du renouveau de la méthode de l'hodographe circulaire que Feynman reprendra à son compte dans ses « lectures on Physics »
*11/ Hamilton va inspirer le [[Théorème de Siacci]] et puis Minkovski qui donnera beaucoup de propriétés des ovales : ceci donne encore une autre démonstration.
=== Goursat et régularisation dite de Levi-Civita ===
*12/. Goursat (1889), Bohlin(1911), AKN {Arnold & Kozlov, Neishtadt} reprennent la méthode z-> sqrt(z) = U (complexe) et le changement d'échelle de temps (dit de Levi-Civita ou de Sundman) dt/dT = 4 |z| : quelques lignes de calcul donnent via le théorème de l'énergie cinétique :
|dU/dT|² = 8 GM + 8 E |U|² ; soit par dérivation
<math> {d^2U \over dT^2} +(-8E)\cdot U = 0</math>, avec E négatif.
Donc U décrit '''une ellipse de Hooke''' et z =sqrt(U) l'ellipse de Kepler.
On aura reconnu en T(t), l'anomalie excentrique. Ce n'est donc qu'une des méthodes précédentes : mais cette méthode a des prolongements plus importants (Cf. [[théorème de Bertrand]]). Voir aussi plus bas.
===régularisation===
cette transformation du problème de Kepler en problème de Hooke est assez stupéfiante. Saari(p141) s'y attarde un peu plus qu'Arnold (Barrow,H,H,Newton) ; peut-être est-ce justifié ; voici :
Le problème de régularisation se pose s'il y a collision , c'est à dire , C très voisin de zéro. Saari dit : la collision entraîne un changement brutal de 2Pi . Afin de garder la particule sur la droite sans singularité , il "suffit de penser" à garder l'arc -moitié ; soit
de changer de jauge (de fonction inconnue): <math>\ U = \sqrt z</math> et de variable (transmutation d'échelle de temps) dT = dt/r(t)(ATTENTION au facteur 4!)
La conservation de l'énergie s'écrit 2|U'|²-1 = Eo.r
et l'équation du mouvement : <math>\ddot z = -z/r^3</math> devient :
<math>r \frac{d^2z}{dT^2} - \frac{dr}{dT}\cdot \frac{dz}{dT} +z = 0</math> ,
équation LINÉAIRE sans le r^3 ! Elle conduit à :
U" -U/r [2|U'|²-1] =0
soit <math>\frac{d^2U}{dT^2}+ (-Eo/2)\cdot U = 0</math> (équation de Hooke).
Le gros avantage de cette solution est qu'elle est stable-numérique : les solutions restent sur la même iso-énergie.
===Kustaanheimo(1924-1997) et Stiefel(1909-1978)===
en 1964, ils utilisèrent les quaternions pour transformer le problème de Kepler dans R^3 en celui de Hooke dans R^3, via R^4! (congrès d'Oberwolfach): ils leur a suffi de prendre la quatrième coordonnées x4 = cste : alors le quaternion U se déplaçait sur la sphère; ceci mit en exergue la symétrie SO(4) et mieux SO(4,2) qui correspondait à la version spinorielle du problème de Kepler (liée à la solution en coordonnées paraboliques) et mettait en avant le vecteur excentricité. Immédiatement, le traitement des perturbations fût amélioré (Stiefel et Scheifel,1971), mais aussi la quantification (methode dite de Pauli (SO(4)), et surtout la quantification lagrangienne SO(4,2),avec ses orbitales "paraboliques" de Kleinert (1967-1998)(cf Kleinert 2006).
*Saari donne des '''raisons topologiques à l'obstruction du passage de R^2 à R^3''' et la nécessité de passer à R^4 (les quaternions): la relation U^2 = z , ne pouvait se régulariser sur la sphère à cause du célèbre théorème du hérisson de Brouwer-Poincaré. Mais si on ne peut "peigner" S2 , on peut peigner S3 (et même S7:octonions), ce qui avec les trois vecteurs tangents donne la fameuse matrice 4-4 de la transformation K-S : rappelons que le maître de Stiefel était Hopf lui-même qui dressa la carte de S3 vers S2 : il n'y a pas de hasard, posséder une bonne formation, cela sert! (cf Oliver(2004)).
----
Voilà donc 12 démonstrations assez mal connues. En existe-t’il d'autres, de cette époque ?
Bien sûr, ont été exclues ici toutes les méthodes de mécanique lagrangienne et hamiltonienne, en particulier celle de [[Max Born]] (cf plus bas).
== Équation du temps, de Kepler : résolution ==
Dans le [[mouvement keplerien]], l''''[[équation du temps, de Kepler]]''' relie l'[[anomalie moyenne]] M = nt à l'[[anomalie excentrique]] E par l'équation
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> M = E - e \cdot \sin E</math>
|}
|
| |
|}</div>
où e est l'[[excentricité orbitale|excentricité]] de la planète.
'''Résoudre cette équation, c'est trouver E(e,M) :'''
* comme série de Fourier puisque c'est une fonction périodique impaire de M
* comme série de puissance de e, si e < eo := 0.6627..., rayon de convergence de la série.
* comme une valeur numérique avec un nombre de chiffres (d), pour un temps de calcul tc(d) optimisé.
=== Série de Fourier ===
C'est [[Joseph-Louis Lagrange|Lagrange]] qui trouve l'expression, bien que le nom J<sub>n</sub>(x) soit associé au nom de [[Friedrich Wilhelm Bessel|Bessel]].
* E-M = fonction impaire périodique de M :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = e\cdot sinE = 2 \cdot \Sigma_{n=1} \frac{J_n(ne)}{n} \sin(nM)</math>
|}
|
| |
|}</div>
'''Démonstration :'''
On rappelle la définition de Jn(z) :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> J_n(z) = \frac{1}{2\pi}\int_0^{\pi} \cos(nx-zsinx)dx</math>
|}
|
| |
|}</div>
et le développement classique de 1/[1-e.cosE] -1 , fonction paire périodique de moyenne nulle vaut:
<math>\Sigma a_n \cdot cos (nM)</math>
avec <math> \ a_n = 2 J_n(ne)</math>
car <math>\pi a_n = \int_0^{2\pi} cos (nM)/(1-e cos E)\cdot dM = \int_0^{2\pi} cos(n[x-esinx]) dx </math>
*On reconnaît (a/r)-1 = 2<math> \Sigma_{n=1}J_n(ne)\cdot \cos (nM)</math>
=== Série entière de l'excentricité ===
C'est encore Lagrange qui trouve la solution en inventant pour l'occasion son théorème d'inversion des fonctions holomorphes ; et [[Pierre-Simon Laplace|Laplace]] donnera le rayon de convergence : mais [[Augustin Louis Cauchy|Cauchy]], pas content du tout, fonde la théorie des séries analytiques pour résoudre ce problème épineux, qui verra son aboutissement avec les travaux de [[Victor Puiseux|Puiseux]].
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = \Sigma_{n=1} {e^n \over n!}\cdot a_n(M)</math>
|}
|
| |
|}</div>
avec <math>\ a_n(M) = D^{n-1} (\sin^n M)</math> et D := opérateur dérivée.
C'est l'application du théorème d'inversion de Lagrange.
*Le rayon de convergence de la série est : eo = 0.6627434193
{{Boîte déroulante|titre= note historique |contenu=
indiqué par Laplace ([[1823]]) et démontré par Cauchy et Puiseux : eo = '''max (x/chx)''' ; soit eo = 1/sh(xo) avec 1/xo = th(xo);démonstration in Wintner, sur l'analyticité de la série.}}
==== Cas des comètes : {{MathText|e > \ e_o|e > eo}} ====
Le premier à se confronter au problème est [[Jeremiah Horrocks|Horrocks]], puis surtout [[Edmond Halley|Halley]] ([[1705]]), pour les calculs sur sa comète d'excentricité e = 0,9673.
Il faut modifier légèrement la solution de Barker (e = 1). Et Bessel([[1805]]) résout ce cas, mais pour e > 0.997
[[Carl Friedrich Gauss|Gauss]] ([[1809]]) s'illustra en donnant une belle solution pour 0,2 < e < 0,95
Autant dire que le voisinage de (0,95 ; 0,98) est fertile en problèmes, en cas d'itération !
=== Calcul numérique ===
Les calculs via les [[intégrateur symplectique]]s exigent de rester toujours en butée du nombre de digits, dans le moindre coût de calcul.<br/>
Depuis 300 ans, on cherche la « meilleure » méthode. Elle reste à trouver !
Bien sûr, cela dépend beaucoup du doublet (M,e), M compris entre 0 et Pi et de e, surtout quand e est voisin de 1.
Nijenhuis (1991) adopte la méthode de Mikkola (1987) qui est la méthode de Newton d'ordre 4, en choisissant « adéquatement » le germe Eo en fonction du doublet (M,e).
Il est clair que dans les calculs numériques, le volume de calculs est essentiel, autant que le nombre de décimales, vu l'instabilité du système solaire évaluée à un [[exposant de Lyapunov|coefficient de Liapunov]] de 10^(t/5Myr). On se heurte à une muraille exponentielle : difficile d'aller plus loin que 25 Myr, même avec un traitement 128 bits.
Ce sont ces calculs (astronomiques... mais informatisés) qui tournent sur les machines de l'IMCCE-Paris. Le calcul de l'ensoleillement terrestre à la latitude 65°Nord, I(65,t) est calculé et on essaie d'en déduire la corrélation avec le climat passé : l'échelle géologique jusqu'au Néogène (25M ans) en est déduite(échelle géologique Gradstein 2004). Prochaine étape prévue : les 65 M ans.
=== Histoire des sciences ===
Avant Kepler, l'équation est déjà étudiée ! bien sûr, pas pour le même problème, mais pour la même équation :
c'est le problème de la réduction des coordonnées locales aux cordonnées géocentriques : il faut réduire la correction de parallaxe. Habash al Hasib s'y est déjà attaqué.
Avant 1700, il y a déjà beaucoup de tentatives : Kepler naturellement, Curtz (1626), Niele, [[Ismaël Bouillau|Bouillau]] (1645, 1657), [[Seth Ward]] (1653), Paganus (1657), Horrebow (1717), [[Jean-Dominique Cassini (Cassini I)|Cassini]] (1669), Newton (1665?), [[Christopher Wren|Wren]] (1658), [[John Wallis|Wallis]] (1659),... De toutes, celle de [[Jeremiah Horrocks]] (1638) est de plus grande beauté. Cf le Colwell, déjà cité.
==== compléments ====
En 1770, Lagrange trouve les deux séries, mais le changement des termes dans les séries le laisse perplexe. 1821 : Cauchy enfin ! Sitôt après, 1824, [[Bessel]] (1784-1846)fera une étude extensive de "ses" fonctions , déjà apparues en 1703 dans une lettre de jean Bernouilli à Leibniz. Daniel Bernouilli fait la théorie du mode propre de la corde suspendue et introduit Jo(x); Euler généralisant a besoin des In(x) , les bessel-modifiées.
*Les calculs de développements approchés donnent :
* E-M = e.sin M[1-e^2/8 +1/192 e^4]+(e²/2). sin(2M)[1-e^2/3 +e^4/24] +e^3.sin (3M)[3/8 -27 e²/128] +e^4/3 .sin(4M)[1-4e²/5] + 125 e^5/384 . sin (5M) + 27 e^6/80 .sin (6M) +O(e^7) (p202 Battin)
* OM/a = 1 - e cos wt +e²/2(1- cos(2wt)) + 3/8e³[cos(wt)-cos(3wt)] + 1/3e⁴[cos(2wt)-cos(4wt)]+ O(e⁵)
* angle POM = θ(t) = wt +2e sin(wt) +5/2 e² sin(2wt) + e³[13/12sin(3wt) -1/4 sin (wt)] +e⁴ [103/96 sin (4wt) -11/24 sin(2wt)] + O(e⁵).
*La solution d'Horrocks(1638) fût :Translater Delphine du déférent de (-2c,0) en D' et prendre E = angle (CP,CD')où C est le centre du déférent
On montre que E(Horrocks) = M + e/1sin M +e²/2 sin 2M +e³/3 sin3M +... et E(H) -E = 1/6 .e³sin³M ; pas si mal!
*La méthode la plus simple est évidemment "regula falsi" (interpolation linéaire inverse ou méthode dite de l'artilleur):
la fonction étant croissante , on "tire" trop bas avec x0 (F(x) est négatif), trop haut avec x1 (F(x) est positif) : alors la racine est entre les deux et on prend la corde.
* On peut montrer que E-M satisfait l'équation cartésienne de Newton : en effet c'est e sin E et donc proportionnelle à y(E)
* (Gudermann(1798-1852)): le cas des orbites hyperboliques se traite par Corinne et donc le Gudermannien :
x = a ch u et y = b sh u ; r = a(1-e ch u)
On pose 1/cos g = ch u et tg g = sh u
soit g = gudermannien (u) = gd(u) = 2 arctg(exp u) -Pi/2.
* Sundman (1873-1949)introduisit en 1912 le temps régularisant :
=== Voir aussi ===
*[[mouvement keplerien]]
*[[intégrateur symplectique]]
*[[Jeremiah Horrocks]], cf discussion.
*Colwell (1993) : solving Kepler's equation over three centuries, ed Willmann-Bell, {{ISBN|0-943396-40-9}}
*Brinkley (1803) : trans roy irish ac, 7,321-356.
== Après Lagrange, jusqu'à Born-Sommerfeld ==
== Les transformations hamiltoniennes du problème de Kepler et SO(4) ==
== En attente , les perturbations , pour faire de mon mieux ==
les perturbations du mvt de Kepler sont parmi les plus "dures" car il y a la dégénérescence banale de SO(3), mais aussi la dégénéréscence de SO(4) pour les états liés : du coup il faut comprendre la structure de la sphère S3 dont on sait qu'elle se retourne comme un gant ou peut se transformer en une foliation torique de Hopf, etc. Comment la perturbation agit sur chacun de ces aspects est encore à inventorier, même si on en connaît pas mal sur le sujet, en particulier gràce aux travaux de Poincaré, KAM, Mather, etc. Il est vrai que le niveau est plus élevé ici, puisqu'il s'agit de problèmes le plus souvent non intégrables.
=== à la manière directe : Danjon-Pollard-Duriez ===
la perturbation F est installée au temps t=0 , avec OMo et Vo donnés , càd Lo,Eo et eo données et passage au périgée donné.On appellera '''ko''' la direction de Lo, et '''uo''' = '''OMo'''/ro, et <math>\vec{u_{\theta_o}}</math> pour compléter le trièdre, dont le vecteur-rotation instantanée sera <math>\vec{\Omega_o}</math> ( '''v''' signifiera donc '''vecteur-vitesse'''). Sept équations sont bien compréhensibles :
* <math>\dot{\vec{L}} = \vec{OM}\wedge \vec{F}</math> (théorème du moment cinétique)
* <math> \dot{E} = \vec{v}\cdot\vec{F}</math> (théorème de l'énerie cinétique)
*<math> \dot{\vec{e}} = \vec{F}\wedge \vec{C} + \vec{v}\wedge \vec{C} </math> (théorème du moment"excentricité")
Moins évidente est la variation de l'anomalie moyenne :
*<math> \omega \cdot a^2 \cdot \dot{M} + \vec{\Omega} \cdot \vec{C} = -2E -2\vec{OM}\cdot \vec{F} </math> que l'on "extrait" du viriel en force.
Il en résulte les équations de Gauss.
==== équations de Gauss ====
le quintuplet [a,e,i,<math>\Omega, \omega </math>]s'en déduit projeté sur le reférentiel initial et final :
* <math>C \cdot \dot{a}= 2a^2\cdot\vec{F}(\vec{u_{\theta} }+e \vec{u_{\theta_o}})</math>
* <math> C \cdot \dot {e} = r (e+cos\theta) \vec{F}\cdot \vec{u_{\theta}}+ p \cdot \vec{F}\cdot \vec{u_{\theta_o}}</math>
* <math> C \cdot (\dot{\omega} +cos( i) \cdot \dot{\Omega}) = r sin \theta \cdot \vec{F}\vec{u_{\theta}} -p \cdot \vec{F} \cdot \vec{u_o}</math> et
* <math>C \cdot (sin (i)\cdot \dot{\Omega}) = r \cdot sin(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math>
* <math> C \cdot\dot{i} = r cos(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math> et bien sûr C varie comme :
*<math>\dot{C} = r \cdot \vec{F}\vec{u_{\theta}} </math>
Et il reste encore dM/dt à écrire !
Comme de plus il faut projeter l perturbation sur la base initiale et la base finale , l'interprétation est sévère.
heureusement, la perturbation dérive souvent d'un potentiel : cela simplifie l'écriture et la compréhension de ces 6 équations, sur lesquelles il faut se pencher qq temps pour les assimiler.
==== pertinence des équations de Gauss ====
demandée ici, pour "souffler un peu" : le cours est construit ainsi ! ne rien faire que l'on ne puisse refaire ou retenir ! Pour retenir, il faut manipuler et croiser les équations jusqu'à ce que cela devienne "machinal" et au fond "intuitif" . Donc la question posée est : en quoi les 6 équations précédentes vous semblent-elles pertinentes ?
{{Boîte déroulante|titre= pertinence des équations de Gauss ; dissertation en 3heures | contenu= d'adord et toujours l'homogénéité ! ensuite prendre des cas particuliers "évidents", etc. }}
=== Perturbation de Kepler : effet Stark classique ===
Si à la force newtonienne vient se rajouter une petite force F, la trajectoire va être légèrement perturbée. Néanmoins si F est parallèle au vecteur excentricité, la symétrie ne sera pas entièrement détruite.
Il convient de prendre les bonnes coordonnées pour traiter ce problème. Comme on sait traiter le mouvement keplerien en [[système de coordonnées paraboliques]], il faut évidemment en profiter.
Mais si F devient trop grand, il apparaît clairement que l'atome va pouvoir s'ioniser plus facilement.
En mécanique quantique cela sera encore plus évident via l'effet tunnel, conduisant à l'ionisation Stark, fragilisant surtout les [[atome de Rydberg]].
=== Mouvement d'Euler à 2 centres d'attraction ===
Euler a vite compris que la composante du vecteur excentricité permettait d'intégrer le problème à 2 soleils fixes et une planète. Cela s'opère grâce à un [[système de coordonnées bifocales]].
Vinti s'est fait le promoteur de cette méthode : ébauche
=== Mouvement si Terre-galette (Béletskii) ===
Beletskii a fait remarquer que le problème d'Euler pouvait s'appliquer à un Soleil légèrement allongé de forme cigare. Par prolongation analytique, avec des masses « imaginaires », il a proposé une interprétation simple du mouvement d'un satellite terrestre sous l'action perturbante du bourrelet (le terme J2(P2(cos(theta)/r³) dans le potentiel gravitationnel. On retrouve les effets décrits dans [[satellite artificiel]].
=== Perturbation de Kepler par planète proche : Terre & Lune ===
Ce problème est ardu : Newton disait que cela lui donnait mal à la tête.
Il a fallu attendre Clairaut (1741) pour avoir une première théorie de la Lune.
Aujourd'hui avec les miroirs posés sur la Lune (Apollo et Lunakhod), on peut comparer la théorie analytique à celle numérique. La précision théorique des LLR (laser lunar range: tir laser vers la Lune) est de quelques centimètres. La théorie analytique comprend plusieurs milliers de termes, mais donne aussi une précision de quelques mètres.
à compléter (séminaire Laskar du 09/03/06).
=== Perturbation de Kepler par planète lointaine : Terre & Jupiter ===
Là, le problème est plus facile . L'essentiel de la méthode consiste en une méthode variable rapide- variable lente, due à Legendre, puis Gauss.
à compléter.
=== Perturbation de Kepler et symétries ===
Bien sûr, chaque fois qu'un système possède une symétrie continue, le théorème de Noether donne une intégrale première, ce qui permet d'éliminer une variable de l'espace des phases.
Comment s'opère cette réduction ?
Le livre de Cordani, celui de Marsden & Ratiu expliquent cette réduction.
Enfin, le problème garde toujours sa symétrie symplectique : il faudra expliquer comment fonctionnent les [[intégrateur symplectique]] (Laskar & Robutel, Celestial Mechanics, 2001,80, 39-62).
------------
== Applications ==
Elles sont innombrables :
*les principales historiquement sont celles de l'astronomie, et prosaïquement des éphémérides solaire et lunaire de notre calendrier des postes.
*les plus utiles sont celles des satellites artificiels.
* le modèle de Rutherford-Bohr de l'atome s'appuie sur cette théorie.
== Perturbations du mouvement de Kepler ==
C'est évidemment essentiel.
Pour les satellites artificiels, il faut tenir compte de la forme non sphérique de la Terre , ET de toutes les autres petites perturbations ( pression de radiation du Soleil sur les panneaux solaires, action de gravité différentielle de la Lune et du Soleil, etc.
Pour l'astronome , il y a essentiellement deux problèmes :
* la perturbation du mouvement Terre-Lune dû au Soleil
* la perturbation de Saturne par Jupiter.
À l'heure actuelle, les programmes de calculs peuvent envisager de traiter (sur un temps pas "trop grand") le mouvement de l'ensemble des planètes. On sait depuis peu que Pluton n'est pas une vraie planète. Ceci dit, le mouvement des planètes sur des échelles de qq 10^6 années commence à être sensible aux conditions initiales (la Terre est un cas particulier car la Lune vient stabiliser son inclinaison et son excentricité).
Pour le programme [[w:Galileo (système de positionnement)|Galileo]] (le [[w:GPS|GPS]] européen), la précision sur le positionnement de la constellation de satellites artificiels est assez impressionnante(inférieure au centimètre).
----
----
== insert provisoire:atome d'hydrogène ==
Cet article suit l'article [[atome d'hydrogène]].
La résolution de l'équation de Schrödinger, écrite en coordonnées polaires, se découple des variables (<math>\theta, \phi</math>) et conduit à une équation à une dimension en r, appelée équation radiale de Leibniz-Schrödinger, puisque ce n'est jamais que la célèbre équation de Leibniz de 1685 traduite en mécanique quantique.
Mais l'équation de Schrödinger (1926) peut se résoudre autrement comme Pauli l'a montré en 1925 !
== Équation radiale ==
L'équation radiale 1D de Leibniz-Schrödinger s'écrit pour r>0:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>-{\hbar^2 \over 2m}S^{''} + ({\hbar^2l(l+1) \over 2r^2} - {e^2 \over r} )S(r) = E \cdot S(r)</math>
|}
|
| |
|}</div>
avec E valeur propre négative ,
et S(r) s'annulant "vite" à l'infini, et S(0) =0 :il s'agit donc d'un problème aux limites dit de Sturm (par opposition à un problème aux conditions initiales, dit de Cauchy); de plus <math>\int_0^{\infty} S^2(r) dr = 1</math>.
[On reconnaît dans <math>{ \hbar^2 l(l+1) \over 2m r^2} </math> la barrière centrifuge de Leibniz (l entier positif) (l=0 correspond à L =0 ; le problème classique n'a pas de correspondant simple en mécanique quantique, encore que ...)].
*Comment arrive-t-on à cette équation '''radiale''' de Leibniz-Schrödinger ?
Il SUFFIT de chercher la fonction d'onde <math>\Psi(x, y, z, t)</math> en coordonnées sphériques sous la forme :
* <math>\Psi = {S(r) \over r}\cdot Y_{l,m} (\theta, \phi) e^{-i{Et \over \hbar}}</math> ,
où les Y(l,m) sont les fonctions [[harmoniques sphériques]]. On appelle ce procédé courant dans les équations aux dérivées partielles, la séparation des variables. Souvent, on appelle R(r) := S(r)/r , la partie radiale de la fonction d'onde.
*'''Note importante annexe''' :
=== Harmoniques sphériques ===
Il n'y a '''rien de mystérieux''' (et surtout rien à voir avec la MÉCANIQUE quantique) dans ce qui semble être un tour de passe-passe. L'étude en électrostatique '''classique''' de l'opérateur Laplacien conduit à ces mêmes fonction Y(l,m) , appelées [[harmoniques sphériques]], qui sont des fonctions '''usuelles''' dès que la symétrie sphérique entre en jeu. L'entier relatif m ne peut prendre que 2l+1 valeurs, de m = -l à m = +l , l étant un entier positif.
Ce sont ces harmoniques sphériques qui "quantifient" le problème sphérique par les deux nombres quantiques l et m (comme il est '''usuel''' dans tout problème de Sturm, dit "aux limites", des équations différentielles), ces deux entiers l et m qui auront tant d'importance dans l'étude de l'[[atome à N électrons]] et donc de la [[Classification périodique]].
*Pour rester en continuité de lecture(sinon voir l'article [[Harmonique sphérique]]), est expliqué ici juste le minimum pour comprendre comment elles interviennent à ce niveau modeste (l=0,1,2,3):les (2l+1)polynômes <math>r^l Y_{l,m}</math> forment une base sur l'ensemble des polynômes homogènes P(x,y,z) de degré l, harmoniques(c’est-à-dire dont le laplacien est nul)
*l=0 :<math>Y_{0,0}= {1\over sqrt(4\pi)}</math> : c'est bien un polynôme de degré zéro, normé sur la sphère unité puisque son carré vaut 1/4Pi.
'''''Dorénavant, nous n'indiquerons plus ce facteur dit de normalisation'''''.
*l=1 :3 fonctions
<math>rY_{1,0} = rcos \vartheta = z</math> ;
<math>rY_{1,1}+rY_{1,-1} = 2rsin \vartheta cos\varphi =2x </math>;
<math>rY_{1,1}-rY_{1,-1} = 2irsin \vartheta sin\varphi =2iy </math>;
soit la base {x,y,z} dite orbitales <math>p_x</math>, <math>p_y</math>, <math>p_z</math>
----
*l=2: cinq fonctions
<math>r^2Y_{2,0} = r^2(3cos^2 \vartheta -1) = 2z^2-x^2-y^2</math> ;
<math>r^2Y_{1,1}+r^2Y_{2,-1} = 2r^2sin \vartheta cos\vartheta cos\varphi = 2xz</math>; et avec moins , 2i yz ;
<math>r^2Y_{2,2}-r^2Y_{2,-2} = 2ir^2sin^2 \vartheta sin2\varphi =4i xy </math>;
<math>r^2Y_{2,2}+r^2Y_{2,-2} = 2ir^2sin^2 \vartheta cos2\varphi =4(x^2-y^2) </math>;
Soit la base {3z^2-r^2, xz, zy, yx, x^2-y^2) dont chaque fonction est de laplacien nul.
----
*l=3: 7 fonctions
soit la base { z(5z^2-3r^2), x(5z^2-3r^2), y(5z^2-3r^2),zxy,z(x^2-y^2),x(x^2-y^2), y(x^2-y^2)}dont chaque fonction est de laplacien nul.
----
* l quelconque : on trouve une base de (2l+1) polynômes réels, mais bien sûr toute combinaison linéaire complexe reste dans ce sous-espace vectoriel sur le corps des complexes.
{{Boîte déroulante|titre=Pourquoi (2l+1)?|contenu=la raison en est aisée :effectuons le décompte : il y a (l+1)(l+2)/2 polynômes homogènes de 3 variables (c'est le nombre de manières d'avoir avec un triplet d'entiers{m,n,p]avec la relation m+n+p = l). Quand on calcule le laplacien on tombe sur l'espace des polynômes homogènes de degré (l-2),de dimension (l-1)l/2 ,pour l >1 ce qui donne pour l'annulation du Laplacien autant de conditions. Donc il ne reste, pour les polynômes homogènes harmoniques qu'un sous-ev de dimension (l²+3l+2 -l²+l)/2 = 2l+1.}}
*Théorème: <math>{P_l(x,y,z) \over r^{l+1}}</math> est fonction propre du laplacien avec la valeur propre -l(l+1):
C'est ce théorème qui est sans arrêt utilisé pour la théorie de l'atome d'hydrogène.
En chimie ,on représente souvent les fonctions 1/r^(l+1) . Pl comme les harmoniques sphériques des orbitales l ; parfois on prend leur carré; etc.
Dans l'[[atome à N électrons]] pour N< 119, l< 5 : donc cela suffit au physicien de l'atome, qui leur a donné des noms et des représentations mnémotechniques diverses. Ne pas oublier que l'on peut combiner à volonté ces fonctions, pour former ce que les chimistes appellent des orbitales hybridées du sous espace propre du niveau d'énergie En( en particulier les fameuses orbitales paraboliques de Kleinert).
=== Multiplicité (2l+1) ===
Le nombre quantique l est appelé '''nombre quantique azimutal''' (on voit qu'il joue, par son terme l(l+1), le même rôle que le carré du moment cinétique, L², en mécanique classique). Évidemment l'équation radiale a ramené le mouvement à UNE seule dimension, la variable radiale, avec la fonction S(r) qui doit s'annuler en r=0 (n'oublions pas c'est S(r)/r qui intervient ) et qui doit être de carré sommable sur l'intervalle r>0 .
On aura donc des valeurs propres de cette équation linéaire, dépendant donc de l , <math> E_{k, l}</math> , mais pas de m (on dit que la multiplicité de la valeur propre est : 2l+1 ; en physique & chimie on dit : il y a dégénérescence du multiplet égale à 2l+1).
Le nombre quantique m s'appelle '''nombre quantique magnétique''', car sous l'effet d'un champ magnétique ([[effet Zeeman]]) l'énergie dépend alors de la valeur de m, et l'on voit une multiplicité de niveaux d'énergie, d'où la dénomination .
Enfin le nombre k , entier positif, s'appelle '''nombre quantique radial''' et donne le nombre de nœuds (k pour knots !) de S(r) pour r > 0 .
Comme la spectroscopie est née un siècle avant la mécanique quantique, la tradition est restée d'appeler le nombre quantique azimutal l par des lettres latines :
l= 1 -> s ; l=2-> p ; l=3 -> d ; l=4 -> f et ensuite g, h .
=== Résultat final ===
Au final, on trouve une énergie E(l,m,k) indépendante de m, soit E(l,k), mais, de façon incroyable (sauf pour Pauli), ne dépendant que de la somme l+k-1 = n , qui doit être un entier positif, et appelé '''nombre quantique principal'''.
C'est la fameuse équation déjà trouvée par Bohr en 1913:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>E_n = {E_1 \over n^2}= -{me^4 \over 2n^2\hbar^2}</math>
|}
|
| |
|}</div>
Il y a ce qu'on appelait une '''dégénérescence accidentelle''', avant l'introduction par Pauli en mécanique quantique du vecteur [[invariant de Runge Lenz]].
La multiplicité, g, du niveau d'énergie En est donc :
pour l variant de 0 à n-1 et
pour m variant de -l à +l
<math> g =\Sigma_0^{n-1} (2l+1)= n^2</math> .
Et, compte-tenu du spin (1/2) de l'électron ,g vaut le double , soit 2.n²
*Ce qui donne simplement : couche K, g=2 ; L, g=8 ; M, g=18 ; O, g= 32 ; P, g = 50 ; Q, g=72 ; R, g = 98 ; S, g= 128.
Inutile d'aller plus loin pour décrire la [[classification périodique]], la configuration de l'élément Z= 119 est celle d'un alcalin :
<math>(1s^2) (2s^2) (2p^6) (3s^2) (3p^6) (4s^2) (3d^{10}) (4p^6)</math> soit Kr(Z=36) puis,
<math>(5s^2) (4d^{10}) (5p^6) (6s^2) (4f^{14}) (5d^{10}) (6p^6)</math> Rn(Z=86) , puis
<math>(7s^2) (5f^{14}) (6d^{10}) (7p^6)</math> Uuo(Z=118),
puis 8s.
Sur les 64 orbitales de la couche S, n= 8 , on n'a besoin de connaître que l'orbitale (8s): ce calcul requiert impérativement la [[mécanique quantique relativiste]] , car les électrons (1s²) de la première couche sont soumis à des vitesses non négligeables devant c .
De même, la configuration de l'élément Z= 121 est Uuo,(8s²,5g), la sous-couche 5g pouvant contenir jusqu'à 2*9 =18 électrons.
- -
Ce faisant, on obtiendra ainsi tous les niveaux d'énergie des éléments '''ET des séries isoélectroniques''', ce qui permettra de décrire certains traits de la [[classification périodique]].
* Pour en revenir à l'atome d'hydrogène, il ne reste plus qu'à introduire le vecteur [[invariant de Runge Lenz]] quantique pour comprendre que la dégénérescence dite "accidentelle" ne l'est pas : il y a bien une symétrie de plus que la simple symétrie centrale dans le cas de ce modèle de Rutherford quantique (cf [[théorème de Bertrand]]).
Auparavant, on va finir le raisonnement de Schrödinger (1926) ; puis on reviendra sur celui, plus subtil, de Pauli (1925).
=== Équation radiale-réduite et Polynômes de Laguerre ===
Si l'on revient à l'équation radiale de Leibniz-Schrödinger, on peut démontrer que pour r voisin de zéro, S(r) varie comme r^(l+1) , et que pour r très grand, S" + 2E S = 0 .
Il est courant de poser 2E = -1/n² et donc S(r) varie comme exp (- r/n) à l'infini : n pour l'instant n'est qu'un réel!
Alors le dernier changement de fonction inconnue est logiquement l'essai suivant qui se révèle fructueux : S(r) = r^(l+1).exp(-r/n).g(r) ; mais on s'aperçoit qu'en changeant la variable r en s : = 2r/n l'équation s'arrange mieux :
L'équation radiale-réduite devient :
s f"(s) + (2l+2-s) f'(s) + (n-l-1) f(s) = 0 , avec g(r) = f(2r/n) = f(s)
Les matheux et Schrödinger ont reconnu cette équation immédiatement (?) : elle conduit à la fonction hypergéométrique dégénérée de Kummer, qui conduit aux [[polynômes de Laguerre]], '''ssi''' n-l-1 est un '''entier positif''' : donc '''n est un entier positif''' et l = 0, 1 , 2 , .. ,n-1 . Et le nombre k est simplement k = n+l-1.
*Pour le cas l= n-1 (les états de Rydberg (cf. [[atome de Rydberg]]), elle devient r .g " + (2n-r) g' = 0 satisfaite par g = cste (en effet S(r) ne doit avoir aucun nœud quand le nombre quantique radial k est nul !).
*Ici, on fera les calculs "à la main" pour les faibles valeurs de n .Mais sinon, les ''aficionados'' des équations différentielles chercheront un développement de f(s) en série entière qui se STOPPE en un polynôme P(s): cela marche, c'est le raisonnement typiquement utilisé avec l'équation hypergéométrique !
=== Infeld-Hull et la "factorisation" ===
dans RevModPhys 23,1951,21-68 , on constate que la méthode des opérateurs d'échelle était bien connue à l'époque (cf aussi Durand, CRAS1950,230,273):
L'idée est classique :
soit A = 1/2 -a/r -d/dr et B = 1/2 - b/r +d/dr en unités "bien choisies".
A et B sont opérateurs sur les fonctions de carrés sommables sur [0, infty[.
Ils sont opérateurs conjugués pour a = b .
et l'équation de Leibniz-S s'écrit :
A(l+1)B(l+1) Snl = (n-l-1)Snl/r
En multipliant par Snl et en sommant il apparaît immédiatement que n-1> l ;
et B S = 0 pour l = n-1 d'où la valeur de S "circulaire" :
S(r) = r^n .exp (-r/2)
Qq calculs permettent de trouver que
S(n+1, l) = r A(n) S(n,l)
S(n-1,l) = rB(n) S(n,l) .1/[(n-1-l)n+l)]
et toutes sortes de relations sur les polynômes de Laguerre.
Noter aussi que l'équation du second ordre peut s'écrire , comme assez souvent :
K(n,l) S(n, l-1) = A S(n,l)
K(n,l) S(n,l) = B S(n, l-1) (Durand p 449)
*Les relations de Pasternak permettant de calculer <r^k > =((n, l,k)) s'en déduisent :
k+1)<r^k> -2n(2k+1)<r^(k-1)> +[(2l+1)²-k²]<r^(k-2)> = 0
*exemples classiques
*(n,l,3) = n²/8[ 35 n^4 -35 n² -30 n²(l+2)(l-1)-3(l+2)(l+1)l(l-1)]
*(n,l,4) = n^4/8[63 n^4 -35n²(2l²+2l-3)+5l(l+1)(3l²+3l -10) +12]
*(n,l,-1) = viriel = 1/n²
*(n,l,-2) = force = 1/n^3(l+1/2)
*(n,l,-3) = force de barrière et LS = 1/n^3(l+1/2)l(l+1)
*(n,l,-4) = ion-dipôle => cf Kondratiev = [3n²-l(l+1)]/2n^5(l-1/2)l(l+1)(l+1/2)(l+3/2)
*noter l=0 pour -3 et -4 ! il faudra être prudent avec les électrons s !
*(n,l,2) = n²(5n²+1-3l(l+1))/2
*(n,l,1) = 3n²-l(l+1)]/2
Certaines se trouvent dans [[atome d'hydrogène]]
== [[Invariant de Runge Lenz]], quantique ==
=== Champ coulombien ===
*Le cas de la force coulombienne (cf. [[mouvement keplerien]] ; le [[puits de potentiel]] a déjà été étudié en mécanique classique) est TRÈS PARTICULIER car il montre que n DOIT être un '''entier positif''', '''indépendant de l''' , alors que les fonctions propres g(n,l,r) dépendent bien de deux indices n et l :
les valeurs propres de l'énergie ne dépendent pas séparément de n et de l , mais '''seulement de n''' , entier positif, qui de ce fait est appelé nombre quantique principal de couche (avec n= 1 -> couche K , n=2 -> couche L ,..).
Ce fait, très exceptionnel pour l'énergie, ne sera plus vrai pour un potentiel V(r) quelconque, même voisin de -e²/r. Il convient donc de ne pas trop s'y attacher, sauf si l'on veut s'expliquer cette dégénérescence (anciennement appelée dégénérescence accidentelle), via le raisonnement de Pauli.
=== vecteur excentricité quantique ===
Le vecteur excentricité (cf. [[mouvement keplerien]] et [[invariant de Runge Lenz]])vaut :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = \vec{V} \wedge \vec {L} / (GMm) -\vec{r}/r </math>
|}
|
| |
|}</div>
Il existe aussi en mécanique quantique, en tant qu'opérateur observable. Il vaut en unités convenables (unités atomiques)
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \hat{\vec e} = [\hat {\vec {p}} \wedge \hat {\vec {L}}-\hat {\vec {L}} \wedge \hat {\vec {p}}]/2- \hat {\vec {r}}/r</math>
|}
|
| |
|}</div>
Or rappelons qu'en termes d'opérateur:
'''p^L +L^p''' = 2i.'''p'''.<math>\hbar</math>
ce qui rend légèrement différent le vecteur quantique , subtilité de l'algèbre non commutative !
=== propriétés de la Q-excentricité ===
Toujours en faisant les calculs d'opérateurs,
on retrouve e.L = 0 , L.e = 0 , e.H = H.e (donc e est bon nombre quantique , et donc dans un sous-ev de la valeur propre de H , e sera stable).
<div style="text-align: center;"><math>e^2 -1 = -(H/E_o)\cdot [L^2/\hbar^2 + 1]</math></div>
Là encore un terme (+1) vient subrepticement se glisser dans les calculs (on a pris Eo = -13.6eV):
Et [e^2,Lz]=0
Mais alors ,dans l'ECOC [H, L², Lz], e² est un bon nombre quantique, et sa valeur est, dans le niveau n :
<div style="text-align: center;">e² = 1 -1/n² -l(l+1)/n²</div>
et par conséquent l ne peut dépasser n-1 ;
Mais on n'attendait pas cette bizarre formule !
=== Boost et Q-excentricité ===
*Et maintenant, la RÉVÉLATION pour tous ceux qui ont fait de la relativité restreinte :
Multiplions le vecteur excentricité par \hbar pour lui donner l'unité d'un moment cinétique et par n par pure commodité dans les calculs.
Nous appellerons ce vecteur <math>\hat{\vec E}</math> ,le vecteur excentricité-boost , qui est un vecteur polaire et non axial.
E commute avec L² , mais pas avec Lz ; et E² est un bon nombre quantique dans l'ECOC [H,L²,Lz], '''mais pas E''' !
MAIS, dans le sous-ev de la couche n ,
<math>[F_{\lambda \mu},F_{\mu \nu}] = F_{\lambda \nu}</math>
où le tenseur antisymétrique 4-4, F est :
(0,E) en première ligne et la matrice 3-3 antisymétrique correspondant à L^ .
VOILA ! l'atome d'hydrogène est invariant par SO(4) [ évidemment pour les états d'énergie positive, par SO(3,1) c'est à dire le groupe de Lorentz ! d'où l'idée de la notation excentricité-boost ! ]: cela était connu de Pauli , de Fock , de Bargmann , etc. Mais à l'époque, peu connaissaient aussi bien que Pauli la relativité restreinte !
Pour démontrer ces relations, il vaut mieux avoir qq notions d'algèbre de Lie (et des formules de trigonométrie correspondantes), car sinon cela peut être un peu long (11 pages dans le X ; et une page dans le Y : X et Y par courtoisie).
=== opérateurs S et D, valeurs propres de H ===
Il "suffit" maintenant de se rendre compte que [H, Lz, Ez] forme un ECOC ( ce qui correspond en mécanique classique aux coordonnées paraboliques et à la vision spinorielle :
soit 2S = L + E et 2D = L- E ;
Alors S² - D² = 0
S et D sont deux moments cinétiques de carrés égaux : s(s+1)
et :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> [S^2 + D^2 + \hbar^2]\hat{H} = E_o \cdot \hbar^2</math>
|}
|
| |
|}</div>
C'EST FINI : H a pour valeurs propres : E_o/n² avec 4s(s+1) +1 = n²
soit n = s+s+1 , donc de dégénérescence : n² (faire ce petit calcul !).
Voici comment depuis 1926, on eût pu enseigner l'atome d'hydrogène de Pauli (Nobel en 1945 après Heisenberg, Schrödinger et Dirac en 1933).
Pourquoi cela ne s'est-il pas produit ? Vraisemblablement parce que les orbitales paraboliques étaient moins utiles que les orbitales- harmoniques sphériques qui privilégiaient donc l'ecoc [H,L²,Lz].
<!--
== Voir aussi ==
* [[atome]]
* [[atome d'hydrogène]]
* [[Théorie de Schrödinger de l'atome d'hydrogène]]
* [[atome à N électrons]]
* [[Classification périodique]]
----
-->
== Compléments sur SO(6)et SO(4,2) ==
vacances closes après avoir vendu mes merguez, je fais le point sur SO(6). Cf Oliver.
SO(6) comporte <math>C_6^2</math> = 15 générateurs de rotation.
(P. Kustaanheimo and E. Stiefel, J. Reine Angew. Math. 218, 204 (1965). )
la transformation K-S amène l'eq de Schrodinger sous une forme simple :
multiplions tout par r :
<math>-1/2 r \Delta +1 = E\cdot r</math>
et opérons le changement de variables ; il vient :
<math>[L_{56}+ L_{46}-2E \cdot (L_{56}- L_{46})-1]|\psi>=0</math>
En utilisant le "tilt" usuel A , tel que -2E = exp2A et les relations de commutation avec L(45) , l'équation se réécrit :
<math>[e^{i A L_{45}}L_{56}e^{-i A L_{45}}-e^{-A}]|\psi>=0</math>
La solution est immédiate :
les vecteurs propres de L(56) sont <math> \ |\phi_n></math> de valeur propres n= 1,2,3,... et donc A = - ln n et on en tire
<math> \ E_n = -1/2n^2</math> ,
puis en opérant la transformation réciproque de K-S , on retrouve les états propres paraboliques <math> \ |n_1,n_2,m></math>, puis via les symboles 3j-de-Wigner , les états sphériques <math>\ |n,l,m></math> (Kleinert p 964):
Que tout cela paraît naïvement facile! Néanmoins rappelons que Feynman avait calé sur ce problème et que le déblocage de situation s'effectua de 1967 à 1998.
== Retour ==
*[[Mécanique, enseignée via l'Histoire des Sciences|Mécanique , enseignée via l'Histoire des Sciences]]
[[Catégorie:Mécanique, enseignée via l'Histoire des Sciences (livre)]]
sjtpydjrj0yxegd2ncs8q69ddx3m4py
763192
763186
2026-04-07T17:15:27Z
Peyraut
123445
/* Le mouvement est central */
763192
wikitext
text/x-wiki
<noinclude>{{Mécanique, enseignée via l'Histoire des Sciences}}</noinclude>
Il s'agit du mouvement d'un point dans un champ central '''F'''('''OM''') = - GMm. '''OM'''/OM³, dit Newtonien.
Kepler en a énoncé les 3 lois principales :
*La planète P a pour trajectoire une ellipse dont le soleil O est un foyer.
*Le rayon vecteur '''OP''' balaye des surfaces égales dans des temps égaux.
*Le carré de la période T du mouvement est comme le cube du grand axe, 2a, de l'ellipse.
La démonstration de ces faits revient à Newton (1684).
L'article mouvement keplerien de la Wiki a été beaucoup modifié.
Nous en rapatrions l'essentiel.
== Le mouvement est central ==
les conséquences immédiates sont :
* Le moment cinétique '''L''' est une constante. On pose '''L''' = m.'''C''')
* Donc la trajectoire est plane, perpendiculaire en O à L<sub>0</sub>
* Dans ce plan , le mouvement tourne autour de O ('''toujours dans le même sens''', choisi comme positif).
* La loi des aires de Kepler est satisfaite : dS/dt = C/2 = 1/2 r².d<math>\theta</math>/dt.
* Comme C est non nul, thêta est une échelle de temps (non linéaire) mais souvent utilisée(cf Note).
* L'hodographe et la trajectoire sont en '''correspondance directe''' : l'un donne l'autre. L'espace des phases sera donc bien R<sup>2</sup> x R<sup>2</sup> , mais de manière très simplifiée.
Note-annexe : historiquement, Ptolémée a utilisé theta' = MF'O = ~ t (+ O(t^3)), car cela suffisait pour les observations de l'époque : cela s'appelle la théorie de l'équant, elle sera vue en exercice.
Note 2 : on a excepté le cas L=0 comme physiquement irréalisable : on doit toujours pouvoir s'y ramener à la limite, et c'est un joli-exercice.
== L'hodographe est un cercle ; donc la trajectoire est une ellipse ==
==='''l'hodographe est un cercle :'''===
Poser p = C<sup>2</sup>/GM (on verra que c'est la longueur du semi-latus-rectum (on dit aussi "paramètre" de l'ellipse), et V<sub>0</sub> = C/p (qui est donc une vitesse, par ailleurs pseudo-scalaire). Alors, on trouve :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{V} = V_0 \times\vec{n}+ \vec{V_1} </math>
|}
|
| |
|}</div>
multiplier par vecteur(k).wedge et diviser par Vo ; on obtient :
=== la trajectoire est l'ellipse : ===
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e}\cdot \vec{r} = p-r </math>
|}
|
| |
|}</div>
===Démonstration :===
prendre comme échelle de temps theta(t) ; le Principe Fondamental de la Dynamique de Translation (PFDT) donne :
<div style="text-align: center;">
<math> \frac{d\vec{V}} {d\theta} = - V_0 \cdot \vec{u} </math>.</div>
donc, par intégration sur la variable theta avec le vecteur unitaire <math>\vec{n}</math> perpendiculaire à <math>\vec{u}</math> :
<div style="text-align: center;"><math>\vec{V} = V_0 \times \vec{n} + \vec{cste}</math>.</div>
soit :
<div style="text-align: center;"><math> \vec{V} = V_0 \times \vec{n} + \vec{V_1}</math>.</div>
Il y a évidemment beaucoup de manière de retrouver le vecteur constant "cste = V1" , en prenant deux valeurs de la vitesse remarquables ; par exemple, la vitesse à l'apogée et au périgée donnent: V(A) = V<sub>0</sub> + V<sub>1</sub> et V(A') = V<sub>0</sub> - V<sub>1</sub> avec V<sub>1</sub> = e×V<sub>0</sub>.
'''''nota bene''''' :''Et Voilà ! C'est fini'' ! L'hodographe est bien un cercle ( de rayon V<sub>0</sub> = C/p) ! La trajectoire sera donc FERMEE ! On obtient donc cette caractéristique FONDAMENTALE du mouvement dès le début du raisonnement. Cette simple remarque a été faite en 1713, mais est passée relativement inaperçue. Il en est résulté des dizaines de re-découvertes ! Jusqu'en 2000, on peut voir des articles ( cf par exemple Butikov, etc.)signalant cette "trouvaille". On peut s'amuser à exploiter cet hodographe, sans doute comme l'a fait Hooke ( tentative dite des elliptoïdes ; rappelons que Hooke n'avait pas grande culture mathématique, mais il avait compris le principe de l'hodographe, puisque c'est cette méthode de l'hodographe qu'il utilise pour l'ellipse dite de Hooke).
==== '''Vecteur excentricité''', {{MathText|\vec{e}|eo}}, constant ====
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = ( \vec{V} \wedge \vec{z})/ V_0-\vec{u} </math>
|}
|
| |
|}</div>
C'est l'extraordinaire intégrale première de Hermann(1713)- retrouvée par Laplace-Runge-Lenz,etc.! Il en sera question plus tard.
La démonstration est immédiate : multiplier l'équation de l'hodographe par r et faire le produit scalaire avec <math>\vec{u}</math>, et la réécrire.
Il faut préciser que <math>\vec{z}</math> est le vecteur unitaire perpendiculaire au plan. On pourrait faire autrement sans ce produit vectoriel mais alors il faudrait ensuite faire le produit scalaire avec la normale et non avec le vecteur rayon, puis multiplier par r.
===='''donc la trajectoire est une ellipse :'''====
Car en multipliant scalairement le vecteur-excentricité <math>\vec{e}</math> par le rayon-vecteur <math>\vec{r}=r\vec{u}</math>, on obtient pour le produit vectoriel :
<math>(\vec{V}\wedge\vec{z}).\vec{r}=rV\sin\alpha=r^2w=C</math>, d'où :
<math>e \cdot r cos\theta = C/V_0-r=p-r</math> , soit :
<div style="text-align: center;">
{| border="0"
|-----
|
{| border="1" cellpadding="10" style="border-collapse:collapse"
|-----
| <math> r = \frac{p}{1 + e \cos \theta} </math>
|}
|
| |
|}</div>
Ce qui est l'équation polaire d'une ellipse d'excentricité e , et de paramètre p , avec le vecteur-excentricité sur l'axe des apsides et la convention '''origine des angles au périgée'''. La valeur de p ( demi-latus rectum := b^2/a := a(1-e^2)) est :
<div style="text-align: center;">
{| border="0"
|-----
|
{| border="1" cellpadding="10" style="border-collapse:collapse"
|-----
| <math> p = \frac{C^2}{(GM)}</math>
|}
|
| |
|}</div>
Évidemment, on peut prendre l'autre convention, '''origine des angles à l'apogée''' ; soit <math> r = \frac{p}{1 - e \cos \theta}</math>,
==='''La conservation de l'énergie''' ===
si l'on introduit l'énergie potentielle -GMm/r , elle conduit à :
1/2 V² - (GM)/r = Eo/m = cste , d'où
<div style="text-align: center;">'''Eo négative == - GMm/2a.'''</div>
'''Exercice''' : montrer que 2a est le grand-axe de l'ellipse.
Donc dans le plan de la trajectoire, les deux quantités physiques '''Lo''' et Eo déterminent la forme de l'ellipse. Bien sûr '''OMo''' et '''Vo''' aussi.
==='''moyens mnémotechniques''' par @d ===
il importe, dans les exercices, de ne pas toujours tout redémontrer, et de savoir retenir les formules encadrées : la méthode d'A.D., dite des d@hus, sert en ce genre de situation :
les seuls paramètres sont cinématiques : GM (constante de Gauss) , Eo/m (énergie massique), et Co (constante des aires).
Donc, un de trop !
'''MAIS''' il suffit de retenir
*p = @d[GM, Co] ( et pas de Eo) ; et de retrouver la constante par le cas particulier du cercle (donc constante = 1)
*2a= @d[GM, Eo/m] ( et pas de Co) ; et de retrouver la constante par le cas particulier du cercle (donc constante = -1)
=== remarque de Hooke-Hamilton ===
Signalons à titre de curiosité ce raisonnement de Hooke, qui a peut-être des résurgences dans la pensée de Allais (Nobel économie quand même !) :
Si l'on considère que le mouvement est plan central, de centre O , pourquoi ne pas dire que la force est centrale et proportionnelle à l'angle balayé par unité de temps, soit <math>\dot{\theta}</math> , alors on retrouve tous les résultats antérieurs. Il est fort possible que ce soit par cette méthode que Hooke ait essayé de retrouver "la fameuse loi en 1/r²" , en appliquant sa méthode du second ordre : se donner la position initiale, puis la position voisine. Alors appliquer la loi et trouver la position ultérieure. Itérer. Il trouva par cette méthode des "elliptoides", ce que méprisa Newton. Plus fin, mais quel mérite en 1820? , Hamilton tirera de cette loi le fait que l'hodographe est un cercle, et tout le reste s'ensuit comme on l'a vu.
Ainsi les lois de Newton seraient simplement liées à un <math>\dot{\theta}</math>. Cette méthode serait plus "économique". Par contre, elle induirait peut-être un malaise, si on l'interprète à la manière Allais, car alors l'interposition de la Lune entre Soleil et Terre pourrait modifier l'angle sous lequel le Soleil serait vu de la Terre, et ainsi modifier "G" : une telle manière de faire serait alors en contradiction avec l'astronomie des trois corps. Il faudrait aussi retrouver la gravimétrie et les "théorèmes remarquables de newton-gauss". Dans cette problématique, on serait alors entraînés fort loin...Cela est bien curieux et ne vaut que pour l'anecdote : il est sain d'avoir toujours des visions différentes ( mais si elles débouchent...sur quelque chose de tangible).
== Mouvement sur la trajectoire ==
* La loi des aires donne S/T = Pi.a.b/T = Co/2 , ce qui donne :
{{exemple||loi de Kepler(1628)|<math>\omega^2 \cdot a^3 = (GM) </math>}}
* Partant du périhélie, et en introduisant l'angle dit [[anomalie excentrique]] E(t)(cf dessin), géométriquement :
<math>tan \theta/2 = tan E/2 \cdot \sqrt \frac{1+e}{1-e}</math>
<math> r = a (1- e \cdot \cos E)</math>;
On calcule géométriquement l'aire balayée depuis le passage au péricentre :
par affinité , S(t) = (b/a)[a²E/2 -ac. sinE /2] = ba(E-e.sinE)/2
Il s'ensuit :
{{exemple||Équation du temps de Kepler|<math>\omega t = E - e \cdot \sin E </math>}}
La fonction réciproque donne E(t), et de là '''OM'''(t).
----
===Fin du Cours===
Il est évident que l'on a cherché ici la compaction maximum du cours.Des dizaines d'ouvrages reprennent ce problème.
Pour nous, 2 ressortent du lot : Chandrasekhar si on aime la géométrie . Tisserand ou Winter si on veut plus exhaustif.Quelques exercices classiques suivent, pour "se faire la main".
----
==Exercices ==
Il y a des dizaines d'exercices sur ce sujet, évidemment très important; soit de satellites artificiels, soit d'astronomie. Nous "essaierons" de les classer.
=== satellites de la Terre ===
'''exMersenne-Descartes-Laplace :'''
Mersenne posa à Descartes la question suivante : si on tire un boulet verticalement, est-il possible que le boulet ne redescende pas?
Soit h = Vo²/2g . Montrer que l'altitude H atteinte est :
1/H = 1/h-1/R . que se passe-t-il pour h > R .
Que penser du cas Vo<c et c²< 2gR (Laplace vers 1800).
----
'''Système d'unités :''' pour la Terre , nous éviterons GM remplacé par gR² avec profit. Du fait de La loi de Galilée, la masse du satellite m n'intervient jamais. On se retrouve donc avec un système d'unités adapté ( un d@hu) tronqué à la cinématique.
*R étant l'unité de longueur, on prendra 2π.R = 40 000 km.
*On conviendra de prendre g = 9,80 m/s².
*La pulsation unitaire sera donc <math>w = \sqrt{\frac{g}{R}}</math>, dite pulsation de Schuler. Il lui correspond une '''période''' <math>T(R)= 2 \pi \sqrt{\frac{R}{g}}</math>, dite période basse altitude (84,4 min).
*La vitesse unitaire est <math>Vo = wR = \sqrt{gR}</math>= 1re vitesse cosmique = 8.2 km/s (vitesse d'un satellite basse altitude).
*L'énergie massique du satellite est donc -1/2 .gR
*Le pivotement sidéral de la Terre est 24h * (365.25/366.25) = 86164 s =17.0 To.En un jour les astronautes voient donc environ 18 fois le Soleil se lever.
En pratique, les satellites d'observation , type Spot orbitent à ~ 800 km d'altitude.
Reprendre le système d'unités de ces satellites.
----
'''Légère erreur de trajectoire :'''
Au lieu de la bonne vitesse Vo de Spot, on donne une vitesse de bonne direction (i.e perpendiculaire au rayon) mais trop forte : V1 = Vo(1+eps). Trouver la trajectoire et la période.
- - - - -
'''Fenêtre de tir :'''
m ex que le précédent mais la bonne vitesse Vo est mal orientée dans le plan d'un angle A , petit. Trouver le périgée.
- - - - -
'''Erreur radiale :'''
m ex que le précédent, mais il y a en sus de Vo , une erreur de vitesse radiale Vo.eps.
----
'''Lâcher-Chute libre :'''
On n' a pas attendu Newton (le 24 Nov 1679) pour réfléchir à la déviation vers l'Est (ou l'ouest!) d'une pierre lâchée de l'équateur; c'était la dispute favorite des Coperniciens et anti-Coperniciens. La vitesse due au pivotement est à l'équateur de 40 000 km/86164 s soit 464 m/s . Selon les anti-Coperniciens, une chute de 5m (environ 1s) eût placé le mobile vers l'Ouest de 464 m ! Galilée (mais il avait tort) disait que le corps tomberait toujours à la verticale. Koyré catalogue les différents types de solutions (chute des graves et mouvement de la Terre): l'imagination au pouvoir ! mais c'est Newton qui donna la solution.
Soit h << R , retrouver le résultat de Newton.
Si h est assez grand, la déviation vers l'est sera si grande que la pierre sera satellite.
Si h = altitude géostationnaire = H , la pierre ne tombe plus !
Si h est encore plus grand , la pierre est à son périgée : elle remonte, périodiquement.
Si h > (R+H) .2^(1/3) - R , qu'arrive-t-il ?
----
'''Balistique :''' voir la WP ( [[ellipse de sûreté]] )
revoir la leçon sur la chute libre avec violence (avec vitesse initiale dit-on aujourd'hui).
Dès que l'on veut une certaine précision (théorique) , il faut tenir compte de ce que la Terre est sphérique et donc prendre comme trajectoire de l'obus une ellipse lancé d'une base B avec une vitesse Vo faisant l'angle A avec la verticale. Soit u = Vo/sqrt(gR).
1/. Relation u et A pour que l'obus tombe à l'antipode.
2/. Déterminer la portée 2R.Beta , via tan B = f(u, tan A).
3/. Pour B donné, combien y a-t-il de trajectoires possibles ? et quelle est la portée maximale.
(Indication : soit H le point d'altitude maximale (pour A=0 !). La trajectoire a pour deuxième foyer un point situé sur le cercle [centre B ; rayon BH]).
----
=== Corrigé des exercices ===
'''Mersenne-Descartes :'''
Appliquer le théorème de l’Énergie cinétique :
-gR²/r +1/2 V² = constante , ce qui conduit au résultat.
Descartes évidemment ne savait rien de tout cela ; mais il se doutait "intuitivement" que si g(z) décroissait alors il y aurait possiblement une "vitesse de libération".
De même , Laplace , très heuristiquement , remarqua que si aucun corps ne pouvait dépasser la vitesse-limite c , alors si c² < 2gR , l'astre serait un trou noir !
Enfin, l'expérience a été tentée ( plus pour tester la relativité galiléenne et/ou la déviation vers l'Est(cf exo plus loin)): bien sûr on n'a jamais retrouvé le boulet! )
----
'''Système d'unités Spot :'''
Ro = 40 000/2Pi +800 = 7166 km.
To via Kepler est : 84.4 (7166/6366)^3/2 = 100 min.
Tout le reste s'en déduit (attention , c'est la pulsation qui a été choisie unitaire).
----
'''Légère erreur de trajectoire :'''
Si eps = sqrt(2) -1 , la trajectoire est parabolique et le satellite part à l'infini.
Sinon , Mo est le périgée: a-c = Ro. D'autre part, E1/m = 1/2 V1² - gR²/Ro ; donc on obtient le grand axe , puis l'apogée en A1 : OA1 = Ro.(V1²/2Vo²-V1²) (On retrouve le cas V1 = Vo.sqrt(2)).
Si eps est petit : l'énergie massique a peu varié : dE/m = mVo².eps . Puis dE/Eo = - da/Ro = -2/3 . dT/To . Donc OA1 = 4Ro.eps et l'excentricité est e = 2eps ; enfin dT = To.3eps
- - - - -
'''Fenêtre de tir :'''
Cette fois, l’Énergie massique n'a pas changé, donc le grand axe vaut 2Ro . Comme OMo = Ro , c'est l'extrémité du petit axe. donc k/\OMo donne la direction du grand axe. La projection de Mo sur celui-ci donne le centre de l'ellipse : l'excentricité vaut donc e = sin A ; d'où le périgée OP1 = Ro(1-sinA) : on ne peut se tromper que de 100 km :cela donne une fenêtre sin A = 100/7166 rad = 0.8°. Assez large , car les pointeurs donnent la seconde d'arc.
- - - - -
'''Erreur radiale :'''
Si eps = 1 , la trajectoire est parabolique !
Cette fois, le moment cinétique Lo est le bon ; donc le paramètre p est le bon . Donc OMo est perpendiculaire au grand axe , dont la direction est connue. Il est facile de calculer le vecteur excentricité qui donne en module eps.
On en déduit a = Ro/(1-eps²) (on retrouve eps = 1 comme limite).
----
'''Lâcher-Chute libre :'''
le Cours donne D = déviation vers l' Est de 2/3.wt.h .
Démontrons-le , façon Newton : la trajectoire est une ellipse , mais où r varie sensiblement comme R+h-1/2gt². La conservation du moment cinétique donne :
d<math>\theta</math>/dt = [(R+h)/R+h-z)]² .w = w (1+ 2z/R),
soit une déviation w.R. int(2z/R) = 1/3 w.gt².t = 2/3 wt.h
Si h= H , c'est l'exercice classique du géostationnaire :
R+H = R .17^(2/3) = 6.6 R = 42 000 km
Si h < H , il existe des trajectoires elliptiques dont Mo est l'apogée : la plus petite aura pour périgée OP = R , donc un grand axe 2a = 2R+H , d'où l'énergie massique . En posant r = Rx , on trouve x^4 + x^3 = 1/2 (289) , soit x = 4.67 et donc h = 3.67 R.
Si h > H , la pierre remonte ! résultat curieux qui aurait sans doute amusé Mersenne, et elle part à l'Ouest (si l'on ose dire).
enfin si h > H. 2^(1/3)= 8.36 R, alors E > 0 , donc trajectoire hyperbolique (limite : parabolique).
----
'''Balistique :'''
V= 8.2km/s := sqrt(gR) a signé le début de la Guerre Froide.
mais déjà les canons longue portée obligeaient à prendre une trajectoire elliptique et non parabolique : 111.111 km c'est déjà 1° à l'équateur!
1/. Si l'obus arrive à l'antipode B' , OB = OB' = paramètre p = Lo²/m²gR² = R soit u.sinA = 1 . (évidemment trajectoire avec A< 45° : il faut une apogée!)
2/. La portée s'évalue en calculant la direction du vecteur-excentricité 1 + i.Lo.Vo.exp(iA)/mgR² = [1-u²sin²A] +i[u²sinAcosA]=> tanB = 1/2 u².sin2A / (1-u²sin²A).
Pertinence : on retrouve Torricelli pour u <<1 ; et le §1.
3/. Pour B donné , équation en tan A :
tan²A (1-u²) - tan A (u²/tanB) + 1 = 0 d'où deux angles B1 et B2 tels que tan(B1+B2) = (tanB1+tanB2)/(1-tanB1.tanB2) = S/(1-P) = -1/tanB, donc A1+A2 = Pi/2+ B : il existe une trajectoire tendue et une plongeante. Portée maximale : tan B = u²/2(1-u²) [pertinent avec u=1 ]
'''Géométriquement''', tout ceci est relatif à la courbe de sûreté qui est l'ellipse de foyers T et B et d'apogée BH (rappel 1/H = 1/h -1/R , exercice sur l’énergie potentielle). En effet , toutes les trajectoires Tr(A) ont m énergie , donc m grand axe , soit TH+HB . Le lieu du deuxième foyer est donc le cercle [centre B, rayon BH]: pour une portée donnée (donc angle B donné , il y a deux solutions : à l'intersection de la droite d'apogée avec ce cercle ; soient F1 et F2 : alors la vitesse initiale étant bissectrice de TBF , les deux vitesses sont telles que A1+A2 = Pi/2+ B. La racine double est lorsque sinB = H/R ( = u²/(2-u²)).
L'ellipse de sûreté est donc telle que MT+MB = HO+HB, et dans ce cas, BM est corde focale [les raisonnements sont calqués sur ceux de Torricelli].
----
=== Exercices d'astronomie ===
==== Étoiles doubles ====
Montrer que dans le cas d'une étoile double, la troisième loi de Kepler s'écrit assez naturellement :
w² . a³ = G (m1+m2)
Que penser des planètes du soleil ?
'''Réponse :'''
Le problème à deux corps donne la réponse : (masse-réduite).w² a = G.m1.m2/a². Ainsi , on obtient une formule symétrique en m1 et m2 , ce qui est pertinent.
Dans le cas des planètes du Soleil , la plus grosse, Jupiter, n'apporte qu'une petite correction m2<< M(Soleil) , ce qui justifie la loi de Kepler. Pour les calculs précis, on fait les corrections, étant entendu que le barycentre du système solaire est quasiment en mouvement uniforme (pour plus de corrections, par exemple pour la ceinture de Kuiper ou le nuage de Oort, il faut envisager la "marée galactique").
==== Conjonction Mars -Terre ====
La distance T-Soleil = 1UA ,période 1an, excentricité e(T); mars-Soleil = d UA,période k ans, excentricité e(M). Montrer que '''TM''': = '''OD''' ne peut varier que dans une couronne >d1 et <d2. Le point D est-il dense dans la couronne? Si k était rationnel := p/q quel serait le mouvement de D.
'''Réponse :'''
Consulter exercices de l'IMCCE.
==== équant de Ptolémée ====
Soit une planète, disons Mars, de trajectoire elliptique d'excentricité e ( = 0.093).
On prend comme échelle de temps l'angle polaire compté à partir du deuxième Foyer F', où "il n'y a rien!".
Est-ce mieux ou moins bien que de compter theta(t) comme temps "uniforme" ?
'''Réponse :'''
Historiquement, cet exercice a beaucoup d'importance : on ne distingue pas un cercle d'une ellipse dès que e<0.1.
Donc Ptolémée croyait que la trajectoire était circulaire. MAIS il avait bien vu que theta(t) n'était pas uniforme ; par contre theta ' (t) l'était à la précision des mesures de l'époque. C'est ce que l'on demande de prouver.
----
== Rapatriement provisoire de la WP:historique de démonstrations ==
Ici est placé tout le travail de recherche historique qui n'intéresse pas forcément tout le monde : il y eût moult "démonstrations" du cours précédent.
=== Newton (1684) ===
*1/. '''la première''', celle de Newton en novembre 1684, est géométrique, le temps étant évalué par l'aire balayée (2ème loi de Kepler) : l'analyse en est faite dans l'[[Exégèse des Principia]].
=== Hermann (1710) ===
*2/. '''la plus simple''' (1710 & 1713) est celle de [[Jakob Hermann]] (1678-1733), élève de [[Jacques Bernoulli]] (1654-1705) : il écrit à [[Jean Bernoulli]] (1667-1748) : on remarque que l'hodographe est un cercle (notion de vecteur excentricité) : en calculant le produit scalaire '''e.r''', on trouve l'ellipse et son péricentre. L'analyse est faite dans [[Invariant de Runge Lenz]].
Laplace la reprendra dans son traité de « Mécanique Céleste ».
Que cela est vite dit dans notre langage moderne ! En réalité, la démonstration géométrique est la remarque classique sur le rôle des podaires dans le cas de champs centraux. Danjon remarque (avec Hamilton) que l'anti-podaire de l'inverse d'un cercle est une conique : cela était enseigné encore au baccalauréat des années 60 (Cf. LEBOSSÉ & EMERY, cours de mathématiques élémentaires).
Quant à Hermann, c'est un tour de force :
Il possède trois intégrales premières en coordonnées cartésiennes tirées de <math>\ddot{x} = -gR^2\cdot x \cdot r^{-3}</math> et idem en y.
* <math>C := x\dot{y} -y\dot{x}</math>
* <math>E_x := \frac{x}{r} - \frac{C}{gR^2}\cdot \dot{y}</math>
* <math>E_y := \frac{y}{r} - \frac{C}{gR^2}\cdot \dot{x}</math>
Éliminer la vitesse : on trouve <math> x \cdot E_x +y\cdot E_y = r- p </math> : c'est une ellipse (Cf.discussion [[conique]], Kepler).
Mais comment a-t-il trouvé les deux intégrales premières du vecteur excentricité ? par un raisonnement analytico-géométrique horriblement compliqué ! On sait aujourd'hui le faire par la théorie de la représentation linéaire des groupes (Moser et SO(4) :1968)
=== Transmutation de la force par Newton ===
*3/. '''la plus surprenante''' est celle de la [[Transmutation de la force]] (Newton, retrouvé par Goursat (1889)): ce théorème est EXTRAORDINAIRE et apprécié des ''afficionados'' des ''Principia''.
=== Keill (1708) ===
* 4/. '''la classique''' : Newton-Keill (en 1708) - Bernoulli (1719)
"Classique", elle est bien "chencitournée".
Le problème est plan, si la force est centrale. Le plan de phase est donc (<math> x,y,\dot{x} , \dot{y}</math>). Les deux équations du PFD (principe fondamental de la dynamique) sont :
<math>\ddot{x} = - \Omega^2 \cdot x</math>
et la même en y.[Évidemment <math>\Omega </math> dépend de r!].
Cette notation est évidemment très réminiscente de celle de Hooke. Mais elle n'a rien à voir, sinon que la symétrie est centrale.
Choisir trois fonctions invariantes par rotation :
*<math>I := 1 \cdot (x^2+ y^2) = r^2</math>, strictement positif,
*<math>J := 1 \cdot (x\dot{x} + y\dot{y})</math>, de sorte que <math> \dot I = 2J (= 2r\dot{r})</math>,
*<math> \ K := {v^2}/{2}</math>, énergie cinétique.
Remarquer cette particularité : r² est choisie comme variable, et non r. Et comme J est non-nulle, I va jouer '''le rôle d'une échelle de temps''' au moins sur une demi-période, du périgée à l'apogée.
Démontrer que le problème se réduit au système différentiel (S) :
*<math>\dot{I} = 2J</math>
*<math>2\dot{J} = K -I \Omega^2(I)/2</math> (th du viriel !)
*<math>\dot{K} = - J \Omega^2(I)</math> (loi de Newton!)
- - -
Keill utilise alors '''l'échelle de temps I''' ; le système se réduit à :
*<math> 4\frac{d(J^2)}{dI} = 2K - I \Omega^2</math>
*<math>\frac {dK}{dI} = - \Omega^2/2</math>
En éliminant Omega² (et quelle que soit sa valeur ! donc c'est vrai pour toute force centrale!)
<math> K = \frac{J^2}{2I} + \frac{C_o^2}{2I}</math>.
C'est un vrai ''tour de force'' : au début du XVIII{{ème}} , on vient de réécrire :
<math>2KI = v^2\cdot r^2 = [\vec r \cdot \vec v]^2 + [\vec r \wedge \vec v]^2 = [\vec r \cdot \vec v]^2 +C_o^2</math>
Emmy Noether connaissait-elle cette démonstration due à l'invariance par rotation ?
- - -
Puis, l'invariance temporelle donne la conservation de l'énergie :
<math>1.\cdot H = K + V(I)</math>, où V(I) est l'énergie potentielle relative à la force centrale (= <math>-\frac{1}{2}\int \Omega^2 dI)</math>
- - -
Ces deux ensembles de surfaces feuillettent l'espace (I,J,K) et leur intersection donne l'orbite du mouvement dans cet espace.
Éliminer K conduit à travailler dans le demi-plan (<math>I, 2J = \dot{I}</math>), c'est à dire dans un plan de phase presque usuel (on joue avec r² plutôt qu'avec r) :
<math> H = \frac{J^2}{2I} + \frac{C_o^2}{2I} + V(I)</math>,
ce qui est '''l'équation de Leibniz(1689)''', mais en notation I = r². (Remarquer que tout résulte de cette circonstance (non évidente du temps où les vecteurs n'existaient pas) :
<math>x \dot{x} + y \dot{y} = r \dot{r}</math>)
et pour finir, comme d'habitude, dt = dI/2J donne le mouvement sur cette orbite de phase et la primitive de 2J(I) donne l'action S(I) du problème.
Évidemment, actuellement, nous repasserions immédiatement en coordonnées (r et r').
Il n'empêche que voilà décrite la solution incroyable de Keill qui témoigne d'une virtuosité tombée dans l'oubli de l'Histoire.
*'''Note d'histoire''':
cette équation ayant été écrite par Lagrange sous cette forme, le H ne saurait signifier « valeur de l'Hamiltonien » ! Peut-être faut-il y voir un hommage à Huygens (?), premier à utiliser la généralisation du théorème de l'énergie cinétique de Torricelli ? peut-être est-ce une simple notation fortuite...
La suite est très classique et correspond à différents paramétrages dans le cas de Kepler :
L'équation de Leibniz se réécrit dans ce cas :
<math> H \cdot 8r^2 -4C_o^2 + 8(GM) \cdot r = 4J^2 </math>
qui est une conique en J et r, ellipse si H est négatif de grand axe <math>2a = - \frac{(GM)}{H}</math> :
Il est usuel alors de paramétrer via l' »anomalie excentrique » :
<math>r = a \cdot(1- e \cos{\phi})</math>,
et « miraculeusement » :
<math>\omega \cdot dt = \frac{r}{a} \cdot d\phi</math> ,
qui s'intègre en donnant la fameuse équation de Kepler.
En contrepartie l'équation en theta est légèrement plus compliquée à intégrer (primitive de <math>\frac{1}{r}</math>) d'où :
<math>tan \frac{\theta}{2} = \sqrt{\frac{1+e}{1-e}} \cdot tan \frac{\phi}{2}</math>.
Note de détail: certains préfèrent la notation i = I/2 , et/ou j = J/2.
=== Clairaut (1741) ===
*5/. '''la méthode de Clairaut''' (1741), reprise par Binet consiste à écrire l'équation de Leibniz à l'aide de u := 1/r :
<math> \dot{r}^2 = 2H + 2gR^2 \cdot u - C^2 \cdot u^2</math>
et cette fois le paramétrage adéquat est :
<math>u := 1+e\cdot \cos \alpha</math> et <math> \dot{r}: = e\cdot \sin \alpha</math>
ce qui conduit au « miraculeux » <math>d \theta = d\alpha</math> ! la trajectoire est donc une ellipse.
Mais la deuxième intégration conduit à <math>dt = k d\alpha \cdot 1/u^2</math> plus difficile à intégrer (mais tout à fait faisable !)
=== Lagrange (1778) ===
*6/. '''la méthode de Lagrange''' est originale (1778) et n'utilise que la linéarité de F = m.a !
Partant de l'équation radiale de Leibniz(1689) :
<math>\ddot{r} = C^2 u^3 - e^2u^2</math>
il pose comme nouvelle variable z = C²-r et trouve :
<math>\ddot{z} = -(GM) \cdot z \cdot u^3 </math>,
'''identique''' aux deux équations de départ en x & y !!
donc il obtient : z (:= C²-r) & x & y linéairement liés, ce qui est la définition d'une ellipse (Cf. [[conique]], discussion). CQFD
=== Laplace (1798) ===
*7/. ''' Laplace''', sans citer Lagrange, calcule, en force brutale, sans aucune intégrale première, l'équation en I = x² + y² du troisième ordre issue du système de Keill : d'où il tire
<math>\frac{d^3I}{dt^3} = - \frac{\dot I}{I^{3/2}}</math>
(comme quoi , le jerk ne date pas d'hier!)
Laplace en tire cette fois '''quatre''' équations '''linéaires''' identiques :
d/dt(r^3.Z") = - Z', avec Z = r, x, y, constante.
D'où r = a x + by + c.constante : c'est une conique !
Il reste à trouver une interprétation physique à ce calcul!
=== Hamilton (1846) et autres ===
*8/. Soit une ellipse ; le foyer F et sa polaire, la directrice (D). Soit P le point courant de l'ellipse et PH sa projection sur la polaire. Le [[théorème de Newton-Hamilton]] donne immédiatement la force centrale F ~ r/PH^3 soit ~ 1/r².e³.
*9/. Hamilton démontre aussi que pour toute mouvement sur une ellipse de paramètre Po, on obtient |'''a/\v'''|.Po = C^3/r^3. Donc si le mouvement est central de foyer F, |a/\v| = a.C/r d'où a ~ 1/r².
*10/. Hamilton est aussi le promoteur du renouveau de la méthode de l'hodographe circulaire que Feynman reprendra à son compte dans ses « lectures on Physics »
*11/ Hamilton va inspirer le [[Théorème de Siacci]] et puis Minkovski qui donnera beaucoup de propriétés des ovales : ceci donne encore une autre démonstration.
=== Goursat et régularisation dite de Levi-Civita ===
*12/. Goursat (1889), Bohlin(1911), AKN {Arnold & Kozlov, Neishtadt} reprennent la méthode z-> sqrt(z) = U (complexe) et le changement d'échelle de temps (dit de Levi-Civita ou de Sundman) dt/dT = 4 |z| : quelques lignes de calcul donnent via le théorème de l'énergie cinétique :
|dU/dT|² = 8 GM + 8 E |U|² ; soit par dérivation
<math> {d^2U \over dT^2} +(-8E)\cdot U = 0</math>, avec E négatif.
Donc U décrit '''une ellipse de Hooke''' et z =sqrt(U) l'ellipse de Kepler.
On aura reconnu en T(t), l'anomalie excentrique. Ce n'est donc qu'une des méthodes précédentes : mais cette méthode a des prolongements plus importants (Cf. [[théorème de Bertrand]]). Voir aussi plus bas.
===régularisation===
cette transformation du problème de Kepler en problème de Hooke est assez stupéfiante. Saari(p141) s'y attarde un peu plus qu'Arnold (Barrow,H,H,Newton) ; peut-être est-ce justifié ; voici :
Le problème de régularisation se pose s'il y a collision , c'est à dire , C très voisin de zéro. Saari dit : la collision entraîne un changement brutal de 2Pi . Afin de garder la particule sur la droite sans singularité , il "suffit de penser" à garder l'arc -moitié ; soit
de changer de jauge (de fonction inconnue): <math>\ U = \sqrt z</math> et de variable (transmutation d'échelle de temps) dT = dt/r(t)(ATTENTION au facteur 4!)
La conservation de l'énergie s'écrit 2|U'|²-1 = Eo.r
et l'équation du mouvement : <math>\ddot z = -z/r^3</math> devient :
<math>r \frac{d^2z}{dT^2} - \frac{dr}{dT}\cdot \frac{dz}{dT} +z = 0</math> ,
équation LINÉAIRE sans le r^3 ! Elle conduit à :
U" -U/r [2|U'|²-1] =0
soit <math>\frac{d^2U}{dT^2}+ (-Eo/2)\cdot U = 0</math> (équation de Hooke).
Le gros avantage de cette solution est qu'elle est stable-numérique : les solutions restent sur la même iso-énergie.
===Kustaanheimo(1924-1997) et Stiefel(1909-1978)===
en 1964, ils utilisèrent les quaternions pour transformer le problème de Kepler dans R^3 en celui de Hooke dans R^3, via R^4! (congrès d'Oberwolfach): ils leur a suffi de prendre la quatrième coordonnées x4 = cste : alors le quaternion U se déplaçait sur la sphère; ceci mit en exergue la symétrie SO(4) et mieux SO(4,2) qui correspondait à la version spinorielle du problème de Kepler (liée à la solution en coordonnées paraboliques) et mettait en avant le vecteur excentricité. Immédiatement, le traitement des perturbations fût amélioré (Stiefel et Scheifel,1971), mais aussi la quantification (methode dite de Pauli (SO(4)), et surtout la quantification lagrangienne SO(4,2),avec ses orbitales "paraboliques" de Kleinert (1967-1998)(cf Kleinert 2006).
*Saari donne des '''raisons topologiques à l'obstruction du passage de R^2 à R^3''' et la nécessité de passer à R^4 (les quaternions): la relation U^2 = z , ne pouvait se régulariser sur la sphère à cause du célèbre théorème du hérisson de Brouwer-Poincaré. Mais si on ne peut "peigner" S2 , on peut peigner S3 (et même S7:octonions), ce qui avec les trois vecteurs tangents donne la fameuse matrice 4-4 de la transformation K-S : rappelons que le maître de Stiefel était Hopf lui-même qui dressa la carte de S3 vers S2 : il n'y a pas de hasard, posséder une bonne formation, cela sert! (cf Oliver(2004)).
----
Voilà donc 12 démonstrations assez mal connues. En existe-t’il d'autres, de cette époque ?
Bien sûr, ont été exclues ici toutes les méthodes de mécanique lagrangienne et hamiltonienne, en particulier celle de [[Max Born]] (cf plus bas).
== Équation du temps, de Kepler : résolution ==
Dans le [[mouvement keplerien]], l''''[[équation du temps, de Kepler]]''' relie l'[[anomalie moyenne]] M = nt à l'[[anomalie excentrique]] E par l'équation
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> M = E - e \cdot \sin E</math>
|}
|
| |
|}</div>
où e est l'[[excentricité orbitale|excentricité]] de la planète.
'''Résoudre cette équation, c'est trouver E(e,M) :'''
* comme série de Fourier puisque c'est une fonction périodique impaire de M
* comme série de puissance de e, si e < eo := 0.6627..., rayon de convergence de la série.
* comme une valeur numérique avec un nombre de chiffres (d), pour un temps de calcul tc(d) optimisé.
=== Série de Fourier ===
C'est [[Joseph-Louis Lagrange|Lagrange]] qui trouve l'expression, bien que le nom J<sub>n</sub>(x) soit associé au nom de [[Friedrich Wilhelm Bessel|Bessel]].
* E-M = fonction impaire périodique de M :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = e\cdot sinE = 2 \cdot \Sigma_{n=1} \frac{J_n(ne)}{n} \sin(nM)</math>
|}
|
| |
|}</div>
'''Démonstration :'''
On rappelle la définition de Jn(z) :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> J_n(z) = \frac{1}{2\pi}\int_0^{\pi} \cos(nx-zsinx)dx</math>
|}
|
| |
|}</div>
et le développement classique de 1/[1-e.cosE] -1 , fonction paire périodique de moyenne nulle vaut:
<math>\Sigma a_n \cdot cos (nM)</math>
avec <math> \ a_n = 2 J_n(ne)</math>
car <math>\pi a_n = \int_0^{2\pi} cos (nM)/(1-e cos E)\cdot dM = \int_0^{2\pi} cos(n[x-esinx]) dx </math>
*On reconnaît (a/r)-1 = 2<math> \Sigma_{n=1}J_n(ne)\cdot \cos (nM)</math>
=== Série entière de l'excentricité ===
C'est encore Lagrange qui trouve la solution en inventant pour l'occasion son théorème d'inversion des fonctions holomorphes ; et [[Pierre-Simon Laplace|Laplace]] donnera le rayon de convergence : mais [[Augustin Louis Cauchy|Cauchy]], pas content du tout, fonde la théorie des séries analytiques pour résoudre ce problème épineux, qui verra son aboutissement avec les travaux de [[Victor Puiseux|Puiseux]].
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> E-M = \Sigma_{n=1} {e^n \over n!}\cdot a_n(M)</math>
|}
|
| |
|}</div>
avec <math>\ a_n(M) = D^{n-1} (\sin^n M)</math> et D := opérateur dérivée.
C'est l'application du théorème d'inversion de Lagrange.
*Le rayon de convergence de la série est : eo = 0.6627434193
{{Boîte déroulante|titre= note historique |contenu=
indiqué par Laplace ([[1823]]) et démontré par Cauchy et Puiseux : eo = '''max (x/chx)''' ; soit eo = 1/sh(xo) avec 1/xo = th(xo);démonstration in Wintner, sur l'analyticité de la série.}}
==== Cas des comètes : {{MathText|e > \ e_o|e > eo}} ====
Le premier à se confronter au problème est [[Jeremiah Horrocks|Horrocks]], puis surtout [[Edmond Halley|Halley]] ([[1705]]), pour les calculs sur sa comète d'excentricité e = 0,9673.
Il faut modifier légèrement la solution de Barker (e = 1). Et Bessel([[1805]]) résout ce cas, mais pour e > 0.997
[[Carl Friedrich Gauss|Gauss]] ([[1809]]) s'illustra en donnant une belle solution pour 0,2 < e < 0,95
Autant dire que le voisinage de (0,95 ; 0,98) est fertile en problèmes, en cas d'itération !
=== Calcul numérique ===
Les calculs via les [[intégrateur symplectique]]s exigent de rester toujours en butée du nombre de digits, dans le moindre coût de calcul.<br/>
Depuis 300 ans, on cherche la « meilleure » méthode. Elle reste à trouver !
Bien sûr, cela dépend beaucoup du doublet (M,e), M compris entre 0 et Pi et de e, surtout quand e est voisin de 1.
Nijenhuis (1991) adopte la méthode de Mikkola (1987) qui est la méthode de Newton d'ordre 4, en choisissant « adéquatement » le germe Eo en fonction du doublet (M,e).
Il est clair que dans les calculs numériques, le volume de calculs est essentiel, autant que le nombre de décimales, vu l'instabilité du système solaire évaluée à un [[exposant de Lyapunov|coefficient de Liapunov]] de 10^(t/5Myr). On se heurte à une muraille exponentielle : difficile d'aller plus loin que 25 Myr, même avec un traitement 128 bits.
Ce sont ces calculs (astronomiques... mais informatisés) qui tournent sur les machines de l'IMCCE-Paris. Le calcul de l'ensoleillement terrestre à la latitude 65°Nord, I(65,t) est calculé et on essaie d'en déduire la corrélation avec le climat passé : l'échelle géologique jusqu'au Néogène (25M ans) en est déduite(échelle géologique Gradstein 2004). Prochaine étape prévue : les 65 M ans.
=== Histoire des sciences ===
Avant Kepler, l'équation est déjà étudiée ! bien sûr, pas pour le même problème, mais pour la même équation :
c'est le problème de la réduction des coordonnées locales aux cordonnées géocentriques : il faut réduire la correction de parallaxe. Habash al Hasib s'y est déjà attaqué.
Avant 1700, il y a déjà beaucoup de tentatives : Kepler naturellement, Curtz (1626), Niele, [[Ismaël Bouillau|Bouillau]] (1645, 1657), [[Seth Ward]] (1653), Paganus (1657), Horrebow (1717), [[Jean-Dominique Cassini (Cassini I)|Cassini]] (1669), Newton (1665?), [[Christopher Wren|Wren]] (1658), [[John Wallis|Wallis]] (1659),... De toutes, celle de [[Jeremiah Horrocks]] (1638) est de plus grande beauté. Cf le Colwell, déjà cité.
==== compléments ====
En 1770, Lagrange trouve les deux séries, mais le changement des termes dans les séries le laisse perplexe. 1821 : Cauchy enfin ! Sitôt après, 1824, [[Bessel]] (1784-1846)fera une étude extensive de "ses" fonctions , déjà apparues en 1703 dans une lettre de jean Bernouilli à Leibniz. Daniel Bernouilli fait la théorie du mode propre de la corde suspendue et introduit Jo(x); Euler généralisant a besoin des In(x) , les bessel-modifiées.
*Les calculs de développements approchés donnent :
* E-M = e.sin M[1-e^2/8 +1/192 e^4]+(e²/2). sin(2M)[1-e^2/3 +e^4/24] +e^3.sin (3M)[3/8 -27 e²/128] +e^4/3 .sin(4M)[1-4e²/5] + 125 e^5/384 . sin (5M) + 27 e^6/80 .sin (6M) +O(e^7) (p202 Battin)
* OM/a = 1 - e cos wt +e²/2(1- cos(2wt)) + 3/8e³[cos(wt)-cos(3wt)] + 1/3e⁴[cos(2wt)-cos(4wt)]+ O(e⁵)
* angle POM = θ(t) = wt +2e sin(wt) +5/2 e² sin(2wt) + e³[13/12sin(3wt) -1/4 sin (wt)] +e⁴ [103/96 sin (4wt) -11/24 sin(2wt)] + O(e⁵).
*La solution d'Horrocks(1638) fût :Translater Delphine du déférent de (-2c,0) en D' et prendre E = angle (CP,CD')où C est le centre du déférent
On montre que E(Horrocks) = M + e/1sin M +e²/2 sin 2M +e³/3 sin3M +... et E(H) -E = 1/6 .e³sin³M ; pas si mal!
*La méthode la plus simple est évidemment "regula falsi" (interpolation linéaire inverse ou méthode dite de l'artilleur):
la fonction étant croissante , on "tire" trop bas avec x0 (F(x) est négatif), trop haut avec x1 (F(x) est positif) : alors la racine est entre les deux et on prend la corde.
* On peut montrer que E-M satisfait l'équation cartésienne de Newton : en effet c'est e sin E et donc proportionnelle à y(E)
* (Gudermann(1798-1852)): le cas des orbites hyperboliques se traite par Corinne et donc le Gudermannien :
x = a ch u et y = b sh u ; r = a(1-e ch u)
On pose 1/cos g = ch u et tg g = sh u
soit g = gudermannien (u) = gd(u) = 2 arctg(exp u) -Pi/2.
* Sundman (1873-1949)introduisit en 1912 le temps régularisant :
=== Voir aussi ===
*[[mouvement keplerien]]
*[[intégrateur symplectique]]
*[[Jeremiah Horrocks]], cf discussion.
*Colwell (1993) : solving Kepler's equation over three centuries, ed Willmann-Bell, {{ISBN|0-943396-40-9}}
*Brinkley (1803) : trans roy irish ac, 7,321-356.
== Après Lagrange, jusqu'à Born-Sommerfeld ==
== Les transformations hamiltoniennes du problème de Kepler et SO(4) ==
== En attente , les perturbations , pour faire de mon mieux ==
les perturbations du mvt de Kepler sont parmi les plus "dures" car il y a la dégénérescence banale de SO(3), mais aussi la dégénéréscence de SO(4) pour les états liés : du coup il faut comprendre la structure de la sphère S3 dont on sait qu'elle se retourne comme un gant ou peut se transformer en une foliation torique de Hopf, etc. Comment la perturbation agit sur chacun de ces aspects est encore à inventorier, même si on en connaît pas mal sur le sujet, en particulier gràce aux travaux de Poincaré, KAM, Mather, etc. Il est vrai que le niveau est plus élevé ici, puisqu'il s'agit de problèmes le plus souvent non intégrables.
=== à la manière directe : Danjon-Pollard-Duriez ===
la perturbation F est installée au temps t=0 , avec OMo et Vo donnés , càd Lo,Eo et eo données et passage au périgée donné.On appellera '''ko''' la direction de Lo, et '''uo''' = '''OMo'''/ro, et <math>\vec{u_{\theta_o}}</math> pour compléter le trièdre, dont le vecteur-rotation instantanée sera <math>\vec{\Omega_o}</math> ( '''v''' signifiera donc '''vecteur-vitesse'''). Sept équations sont bien compréhensibles :
* <math>\dot{\vec{L}} = \vec{OM}\wedge \vec{F}</math> (théorème du moment cinétique)
* <math> \dot{E} = \vec{v}\cdot\vec{F}</math> (théorème de l'énerie cinétique)
*<math> \dot{\vec{e}} = \vec{F}\wedge \vec{C} + \vec{v}\wedge \vec{C} </math> (théorème du moment"excentricité")
Moins évidente est la variation de l'anomalie moyenne :
*<math> \omega \cdot a^2 \cdot \dot{M} + \vec{\Omega} \cdot \vec{C} = -2E -2\vec{OM}\cdot \vec{F} </math> que l'on "extrait" du viriel en force.
Il en résulte les équations de Gauss.
==== équations de Gauss ====
le quintuplet [a,e,i,<math>\Omega, \omega </math>]s'en déduit projeté sur le reférentiel initial et final :
* <math>C \cdot \dot{a}= 2a^2\cdot\vec{F}(\vec{u_{\theta} }+e \vec{u_{\theta_o}})</math>
* <math> C \cdot \dot {e} = r (e+cos\theta) \vec{F}\cdot \vec{u_{\theta}}+ p \cdot \vec{F}\cdot \vec{u_{\theta_o}}</math>
* <math> C \cdot (\dot{\omega} +cos( i) \cdot \dot{\Omega}) = r sin \theta \cdot \vec{F}\vec{u_{\theta}} -p \cdot \vec{F} \cdot \vec{u_o}</math> et
* <math>C \cdot (sin (i)\cdot \dot{\Omega}) = r \cdot sin(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math>
* <math> C \cdot\dot{i} = r cos(\omega +\theta)\cdot (\vec{F}\cdot \vec{k}) </math> et bien sûr C varie comme :
*<math>\dot{C} = r \cdot \vec{F}\vec{u_{\theta}} </math>
Et il reste encore dM/dt à écrire !
Comme de plus il faut projeter l perturbation sur la base initiale et la base finale , l'interprétation est sévère.
heureusement, la perturbation dérive souvent d'un potentiel : cela simplifie l'écriture et la compréhension de ces 6 équations, sur lesquelles il faut se pencher qq temps pour les assimiler.
==== pertinence des équations de Gauss ====
demandée ici, pour "souffler un peu" : le cours est construit ainsi ! ne rien faire que l'on ne puisse refaire ou retenir ! Pour retenir, il faut manipuler et croiser les équations jusqu'à ce que cela devienne "machinal" et au fond "intuitif" . Donc la question posée est : en quoi les 6 équations précédentes vous semblent-elles pertinentes ?
{{Boîte déroulante|titre= pertinence des équations de Gauss ; dissertation en 3heures | contenu= d'adord et toujours l'homogénéité ! ensuite prendre des cas particuliers "évidents", etc. }}
=== Perturbation de Kepler : effet Stark classique ===
Si à la force newtonienne vient se rajouter une petite force F, la trajectoire va être légèrement perturbée. Néanmoins si F est parallèle au vecteur excentricité, la symétrie ne sera pas entièrement détruite.
Il convient de prendre les bonnes coordonnées pour traiter ce problème. Comme on sait traiter le mouvement keplerien en [[système de coordonnées paraboliques]], il faut évidemment en profiter.
Mais si F devient trop grand, il apparaît clairement que l'atome va pouvoir s'ioniser plus facilement.
En mécanique quantique cela sera encore plus évident via l'effet tunnel, conduisant à l'ionisation Stark, fragilisant surtout les [[atome de Rydberg]].
=== Mouvement d'Euler à 2 centres d'attraction ===
Euler a vite compris que la composante du vecteur excentricité permettait d'intégrer le problème à 2 soleils fixes et une planète. Cela s'opère grâce à un [[système de coordonnées bifocales]].
Vinti s'est fait le promoteur de cette méthode : ébauche
=== Mouvement si Terre-galette (Béletskii) ===
Beletskii a fait remarquer que le problème d'Euler pouvait s'appliquer à un Soleil légèrement allongé de forme cigare. Par prolongation analytique, avec des masses « imaginaires », il a proposé une interprétation simple du mouvement d'un satellite terrestre sous l'action perturbante du bourrelet (le terme J2(P2(cos(theta)/r³) dans le potentiel gravitationnel. On retrouve les effets décrits dans [[satellite artificiel]].
=== Perturbation de Kepler par planète proche : Terre & Lune ===
Ce problème est ardu : Newton disait que cela lui donnait mal à la tête.
Il a fallu attendre Clairaut (1741) pour avoir une première théorie de la Lune.
Aujourd'hui avec les miroirs posés sur la Lune (Apollo et Lunakhod), on peut comparer la théorie analytique à celle numérique. La précision théorique des LLR (laser lunar range: tir laser vers la Lune) est de quelques centimètres. La théorie analytique comprend plusieurs milliers de termes, mais donne aussi une précision de quelques mètres.
à compléter (séminaire Laskar du 09/03/06).
=== Perturbation de Kepler par planète lointaine : Terre & Jupiter ===
Là, le problème est plus facile . L'essentiel de la méthode consiste en une méthode variable rapide- variable lente, due à Legendre, puis Gauss.
à compléter.
=== Perturbation de Kepler et symétries ===
Bien sûr, chaque fois qu'un système possède une symétrie continue, le théorème de Noether donne une intégrale première, ce qui permet d'éliminer une variable de l'espace des phases.
Comment s'opère cette réduction ?
Le livre de Cordani, celui de Marsden & Ratiu expliquent cette réduction.
Enfin, le problème garde toujours sa symétrie symplectique : il faudra expliquer comment fonctionnent les [[intégrateur symplectique]] (Laskar & Robutel, Celestial Mechanics, 2001,80, 39-62).
------------
== Applications ==
Elles sont innombrables :
*les principales historiquement sont celles de l'astronomie, et prosaïquement des éphémérides solaire et lunaire de notre calendrier des postes.
*les plus utiles sont celles des satellites artificiels.
* le modèle de Rutherford-Bohr de l'atome s'appuie sur cette théorie.
== Perturbations du mouvement de Kepler ==
C'est évidemment essentiel.
Pour les satellites artificiels, il faut tenir compte de la forme non sphérique de la Terre , ET de toutes les autres petites perturbations ( pression de radiation du Soleil sur les panneaux solaires, action de gravité différentielle de la Lune et du Soleil, etc.
Pour l'astronome , il y a essentiellement deux problèmes :
* la perturbation du mouvement Terre-Lune dû au Soleil
* la perturbation de Saturne par Jupiter.
À l'heure actuelle, les programmes de calculs peuvent envisager de traiter (sur un temps pas "trop grand") le mouvement de l'ensemble des planètes. On sait depuis peu que Pluton n'est pas une vraie planète. Ceci dit, le mouvement des planètes sur des échelles de qq 10^6 années commence à être sensible aux conditions initiales (la Terre est un cas particulier car la Lune vient stabiliser son inclinaison et son excentricité).
Pour le programme [[w:Galileo (système de positionnement)|Galileo]] (le [[w:GPS|GPS]] européen), la précision sur le positionnement de la constellation de satellites artificiels est assez impressionnante(inférieure au centimètre).
----
----
== insert provisoire:atome d'hydrogène ==
Cet article suit l'article [[atome d'hydrogène]].
La résolution de l'équation de Schrödinger, écrite en coordonnées polaires, se découple des variables (<math>\theta, \phi</math>) et conduit à une équation à une dimension en r, appelée équation radiale de Leibniz-Schrödinger, puisque ce n'est jamais que la célèbre équation de Leibniz de 1685 traduite en mécanique quantique.
Mais l'équation de Schrödinger (1926) peut se résoudre autrement comme Pauli l'a montré en 1925 !
== Équation radiale ==
L'équation radiale 1D de Leibniz-Schrödinger s'écrit pour r>0:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>-{\hbar^2 \over 2m}S^{''} + ({\hbar^2l(l+1) \over 2r^2} - {e^2 \over r} )S(r) = E \cdot S(r)</math>
|}
|
| |
|}</div>
avec E valeur propre négative ,
et S(r) s'annulant "vite" à l'infini, et S(0) =0 :il s'agit donc d'un problème aux limites dit de Sturm (par opposition à un problème aux conditions initiales, dit de Cauchy); de plus <math>\int_0^{\infty} S^2(r) dr = 1</math>.
[On reconnaît dans <math>{ \hbar^2 l(l+1) \over 2m r^2} </math> la barrière centrifuge de Leibniz (l entier positif) (l=0 correspond à L =0 ; le problème classique n'a pas de correspondant simple en mécanique quantique, encore que ...)].
*Comment arrive-t-on à cette équation '''radiale''' de Leibniz-Schrödinger ?
Il SUFFIT de chercher la fonction d'onde <math>\Psi(x, y, z, t)</math> en coordonnées sphériques sous la forme :
* <math>\Psi = {S(r) \over r}\cdot Y_{l,m} (\theta, \phi) e^{-i{Et \over \hbar}}</math> ,
où les Y(l,m) sont les fonctions [[harmoniques sphériques]]. On appelle ce procédé courant dans les équations aux dérivées partielles, la séparation des variables. Souvent, on appelle R(r) := S(r)/r , la partie radiale de la fonction d'onde.
*'''Note importante annexe''' :
=== Harmoniques sphériques ===
Il n'y a '''rien de mystérieux''' (et surtout rien à voir avec la MÉCANIQUE quantique) dans ce qui semble être un tour de passe-passe. L'étude en électrostatique '''classique''' de l'opérateur Laplacien conduit à ces mêmes fonction Y(l,m) , appelées [[harmoniques sphériques]], qui sont des fonctions '''usuelles''' dès que la symétrie sphérique entre en jeu. L'entier relatif m ne peut prendre que 2l+1 valeurs, de m = -l à m = +l , l étant un entier positif.
Ce sont ces harmoniques sphériques qui "quantifient" le problème sphérique par les deux nombres quantiques l et m (comme il est '''usuel''' dans tout problème de Sturm, dit "aux limites", des équations différentielles), ces deux entiers l et m qui auront tant d'importance dans l'étude de l'[[atome à N électrons]] et donc de la [[Classification périodique]].
*Pour rester en continuité de lecture(sinon voir l'article [[Harmonique sphérique]]), est expliqué ici juste le minimum pour comprendre comment elles interviennent à ce niveau modeste (l=0,1,2,3):les (2l+1)polynômes <math>r^l Y_{l,m}</math> forment une base sur l'ensemble des polynômes homogènes P(x,y,z) de degré l, harmoniques(c’est-à-dire dont le laplacien est nul)
*l=0 :<math>Y_{0,0}= {1\over sqrt(4\pi)}</math> : c'est bien un polynôme de degré zéro, normé sur la sphère unité puisque son carré vaut 1/4Pi.
'''''Dorénavant, nous n'indiquerons plus ce facteur dit de normalisation'''''.
*l=1 :3 fonctions
<math>rY_{1,0} = rcos \vartheta = z</math> ;
<math>rY_{1,1}+rY_{1,-1} = 2rsin \vartheta cos\varphi =2x </math>;
<math>rY_{1,1}-rY_{1,-1} = 2irsin \vartheta sin\varphi =2iy </math>;
soit la base {x,y,z} dite orbitales <math>p_x</math>, <math>p_y</math>, <math>p_z</math>
----
*l=2: cinq fonctions
<math>r^2Y_{2,0} = r^2(3cos^2 \vartheta -1) = 2z^2-x^2-y^2</math> ;
<math>r^2Y_{1,1}+r^2Y_{2,-1} = 2r^2sin \vartheta cos\vartheta cos\varphi = 2xz</math>; et avec moins , 2i yz ;
<math>r^2Y_{2,2}-r^2Y_{2,-2} = 2ir^2sin^2 \vartheta sin2\varphi =4i xy </math>;
<math>r^2Y_{2,2}+r^2Y_{2,-2} = 2ir^2sin^2 \vartheta cos2\varphi =4(x^2-y^2) </math>;
Soit la base {3z^2-r^2, xz, zy, yx, x^2-y^2) dont chaque fonction est de laplacien nul.
----
*l=3: 7 fonctions
soit la base { z(5z^2-3r^2), x(5z^2-3r^2), y(5z^2-3r^2),zxy,z(x^2-y^2),x(x^2-y^2), y(x^2-y^2)}dont chaque fonction est de laplacien nul.
----
* l quelconque : on trouve une base de (2l+1) polynômes réels, mais bien sûr toute combinaison linéaire complexe reste dans ce sous-espace vectoriel sur le corps des complexes.
{{Boîte déroulante|titre=Pourquoi (2l+1)?|contenu=la raison en est aisée :effectuons le décompte : il y a (l+1)(l+2)/2 polynômes homogènes de 3 variables (c'est le nombre de manières d'avoir avec un triplet d'entiers{m,n,p]avec la relation m+n+p = l). Quand on calcule le laplacien on tombe sur l'espace des polynômes homogènes de degré (l-2),de dimension (l-1)l/2 ,pour l >1 ce qui donne pour l'annulation du Laplacien autant de conditions. Donc il ne reste, pour les polynômes homogènes harmoniques qu'un sous-ev de dimension (l²+3l+2 -l²+l)/2 = 2l+1.}}
*Théorème: <math>{P_l(x,y,z) \over r^{l+1}}</math> est fonction propre du laplacien avec la valeur propre -l(l+1):
C'est ce théorème qui est sans arrêt utilisé pour la théorie de l'atome d'hydrogène.
En chimie ,on représente souvent les fonctions 1/r^(l+1) . Pl comme les harmoniques sphériques des orbitales l ; parfois on prend leur carré; etc.
Dans l'[[atome à N électrons]] pour N< 119, l< 5 : donc cela suffit au physicien de l'atome, qui leur a donné des noms et des représentations mnémotechniques diverses. Ne pas oublier que l'on peut combiner à volonté ces fonctions, pour former ce que les chimistes appellent des orbitales hybridées du sous espace propre du niveau d'énergie En( en particulier les fameuses orbitales paraboliques de Kleinert).
=== Multiplicité (2l+1) ===
Le nombre quantique l est appelé '''nombre quantique azimutal''' (on voit qu'il joue, par son terme l(l+1), le même rôle que le carré du moment cinétique, L², en mécanique classique). Évidemment l'équation radiale a ramené le mouvement à UNE seule dimension, la variable radiale, avec la fonction S(r) qui doit s'annuler en r=0 (n'oublions pas c'est S(r)/r qui intervient ) et qui doit être de carré sommable sur l'intervalle r>0 .
On aura donc des valeurs propres de cette équation linéaire, dépendant donc de l , <math> E_{k, l}</math> , mais pas de m (on dit que la multiplicité de la valeur propre est : 2l+1 ; en physique & chimie on dit : il y a dégénérescence du multiplet égale à 2l+1).
Le nombre quantique m s'appelle '''nombre quantique magnétique''', car sous l'effet d'un champ magnétique ([[effet Zeeman]]) l'énergie dépend alors de la valeur de m, et l'on voit une multiplicité de niveaux d'énergie, d'où la dénomination .
Enfin le nombre k , entier positif, s'appelle '''nombre quantique radial''' et donne le nombre de nœuds (k pour knots !) de S(r) pour r > 0 .
Comme la spectroscopie est née un siècle avant la mécanique quantique, la tradition est restée d'appeler le nombre quantique azimutal l par des lettres latines :
l= 1 -> s ; l=2-> p ; l=3 -> d ; l=4 -> f et ensuite g, h .
=== Résultat final ===
Au final, on trouve une énergie E(l,m,k) indépendante de m, soit E(l,k), mais, de façon incroyable (sauf pour Pauli), ne dépendant que de la somme l+k-1 = n , qui doit être un entier positif, et appelé '''nombre quantique principal'''.
C'est la fameuse équation déjà trouvée par Bohr en 1913:
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math>E_n = {E_1 \over n^2}= -{me^4 \over 2n^2\hbar^2}</math>
|}
|
| |
|}</div>
Il y a ce qu'on appelait une '''dégénérescence accidentelle''', avant l'introduction par Pauli en mécanique quantique du vecteur [[invariant de Runge Lenz]].
La multiplicité, g, du niveau d'énergie En est donc :
pour l variant de 0 à n-1 et
pour m variant de -l à +l
<math> g =\Sigma_0^{n-1} (2l+1)= n^2</math> .
Et, compte-tenu du spin (1/2) de l'électron ,g vaut le double , soit 2.n²
*Ce qui donne simplement : couche K, g=2 ; L, g=8 ; M, g=18 ; O, g= 32 ; P, g = 50 ; Q, g=72 ; R, g = 98 ; S, g= 128.
Inutile d'aller plus loin pour décrire la [[classification périodique]], la configuration de l'élément Z= 119 est celle d'un alcalin :
<math>(1s^2) (2s^2) (2p^6) (3s^2) (3p^6) (4s^2) (3d^{10}) (4p^6)</math> soit Kr(Z=36) puis,
<math>(5s^2) (4d^{10}) (5p^6) (6s^2) (4f^{14}) (5d^{10}) (6p^6)</math> Rn(Z=86) , puis
<math>(7s^2) (5f^{14}) (6d^{10}) (7p^6)</math> Uuo(Z=118),
puis 8s.
Sur les 64 orbitales de la couche S, n= 8 , on n'a besoin de connaître que l'orbitale (8s): ce calcul requiert impérativement la [[mécanique quantique relativiste]] , car les électrons (1s²) de la première couche sont soumis à des vitesses non négligeables devant c .
De même, la configuration de l'élément Z= 121 est Uuo,(8s²,5g), la sous-couche 5g pouvant contenir jusqu'à 2*9 =18 électrons.
- -
Ce faisant, on obtiendra ainsi tous les niveaux d'énergie des éléments '''ET des séries isoélectroniques''', ce qui permettra de décrire certains traits de la [[classification périodique]].
* Pour en revenir à l'atome d'hydrogène, il ne reste plus qu'à introduire le vecteur [[invariant de Runge Lenz]] quantique pour comprendre que la dégénérescence dite "accidentelle" ne l'est pas : il y a bien une symétrie de plus que la simple symétrie centrale dans le cas de ce modèle de Rutherford quantique (cf [[théorème de Bertrand]]).
Auparavant, on va finir le raisonnement de Schrödinger (1926) ; puis on reviendra sur celui, plus subtil, de Pauli (1925).
=== Équation radiale-réduite et Polynômes de Laguerre ===
Si l'on revient à l'équation radiale de Leibniz-Schrödinger, on peut démontrer que pour r voisin de zéro, S(r) varie comme r^(l+1) , et que pour r très grand, S" + 2E S = 0 .
Il est courant de poser 2E = -1/n² et donc S(r) varie comme exp (- r/n) à l'infini : n pour l'instant n'est qu'un réel!
Alors le dernier changement de fonction inconnue est logiquement l'essai suivant qui se révèle fructueux : S(r) = r^(l+1).exp(-r/n).g(r) ; mais on s'aperçoit qu'en changeant la variable r en s : = 2r/n l'équation s'arrange mieux :
L'équation radiale-réduite devient :
s f"(s) + (2l+2-s) f'(s) + (n-l-1) f(s) = 0 , avec g(r) = f(2r/n) = f(s)
Les matheux et Schrödinger ont reconnu cette équation immédiatement (?) : elle conduit à la fonction hypergéométrique dégénérée de Kummer, qui conduit aux [[polynômes de Laguerre]], '''ssi''' n-l-1 est un '''entier positif''' : donc '''n est un entier positif''' et l = 0, 1 , 2 , .. ,n-1 . Et le nombre k est simplement k = n+l-1.
*Pour le cas l= n-1 (les états de Rydberg (cf. [[atome de Rydberg]]), elle devient r .g " + (2n-r) g' = 0 satisfaite par g = cste (en effet S(r) ne doit avoir aucun nœud quand le nombre quantique radial k est nul !).
*Ici, on fera les calculs "à la main" pour les faibles valeurs de n .Mais sinon, les ''aficionados'' des équations différentielles chercheront un développement de f(s) en série entière qui se STOPPE en un polynôme P(s): cela marche, c'est le raisonnement typiquement utilisé avec l'équation hypergéométrique !
=== Infeld-Hull et la "factorisation" ===
dans RevModPhys 23,1951,21-68 , on constate que la méthode des opérateurs d'échelle était bien connue à l'époque (cf aussi Durand, CRAS1950,230,273):
L'idée est classique :
soit A = 1/2 -a/r -d/dr et B = 1/2 - b/r +d/dr en unités "bien choisies".
A et B sont opérateurs sur les fonctions de carrés sommables sur [0, infty[.
Ils sont opérateurs conjugués pour a = b .
et l'équation de Leibniz-S s'écrit :
A(l+1)B(l+1) Snl = (n-l-1)Snl/r
En multipliant par Snl et en sommant il apparaît immédiatement que n-1> l ;
et B S = 0 pour l = n-1 d'où la valeur de S "circulaire" :
S(r) = r^n .exp (-r/2)
Qq calculs permettent de trouver que
S(n+1, l) = r A(n) S(n,l)
S(n-1,l) = rB(n) S(n,l) .1/[(n-1-l)n+l)]
et toutes sortes de relations sur les polynômes de Laguerre.
Noter aussi que l'équation du second ordre peut s'écrire , comme assez souvent :
K(n,l) S(n, l-1) = A S(n,l)
K(n,l) S(n,l) = B S(n, l-1) (Durand p 449)
*Les relations de Pasternak permettant de calculer <r^k > =((n, l,k)) s'en déduisent :
k+1)<r^k> -2n(2k+1)<r^(k-1)> +[(2l+1)²-k²]<r^(k-2)> = 0
*exemples classiques
*(n,l,3) = n²/8[ 35 n^4 -35 n² -30 n²(l+2)(l-1)-3(l+2)(l+1)l(l-1)]
*(n,l,4) = n^4/8[63 n^4 -35n²(2l²+2l-3)+5l(l+1)(3l²+3l -10) +12]
*(n,l,-1) = viriel = 1/n²
*(n,l,-2) = force = 1/n^3(l+1/2)
*(n,l,-3) = force de barrière et LS = 1/n^3(l+1/2)l(l+1)
*(n,l,-4) = ion-dipôle => cf Kondratiev = [3n²-l(l+1)]/2n^5(l-1/2)l(l+1)(l+1/2)(l+3/2)
*noter l=0 pour -3 et -4 ! il faudra être prudent avec les électrons s !
*(n,l,2) = n²(5n²+1-3l(l+1))/2
*(n,l,1) = 3n²-l(l+1)]/2
Certaines se trouvent dans [[atome d'hydrogène]]
== [[Invariant de Runge Lenz]], quantique ==
=== Champ coulombien ===
*Le cas de la force coulombienne (cf. [[mouvement keplerien]] ; le [[puits de potentiel]] a déjà été étudié en mécanique classique) est TRÈS PARTICULIER car il montre que n DOIT être un '''entier positif''', '''indépendant de l''' , alors que les fonctions propres g(n,l,r) dépendent bien de deux indices n et l :
les valeurs propres de l'énergie ne dépendent pas séparément de n et de l , mais '''seulement de n''' , entier positif, qui de ce fait est appelé nombre quantique principal de couche (avec n= 1 -> couche K , n=2 -> couche L ,..).
Ce fait, très exceptionnel pour l'énergie, ne sera plus vrai pour un potentiel V(r) quelconque, même voisin de -e²/r. Il convient donc de ne pas trop s'y attacher, sauf si l'on veut s'expliquer cette dégénérescence (anciennement appelée dégénérescence accidentelle), via le raisonnement de Pauli.
=== vecteur excentricité quantique ===
Le vecteur excentricité (cf. [[mouvement keplerien]] et [[invariant de Runge Lenz]])vaut :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \vec{e} = \vec{V} \wedge \vec {L} / (GMm) -\vec{r}/r </math>
|}
|
| |
|}</div>
Il existe aussi en mécanique quantique, en tant qu'opérateur observable. Il vaut en unités convenables (unités atomiques)
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> \hat{\vec e} = [\hat {\vec {p}} \wedge \hat {\vec {L}}-\hat {\vec {L}} \wedge \hat {\vec {p}}]/2- \hat {\vec {r}}/r</math>
|}
|
| |
|}</div>
Or rappelons qu'en termes d'opérateur:
'''p^L +L^p''' = 2i.'''p'''.<math>\hbar</math>
ce qui rend légèrement différent le vecteur quantique , subtilité de l'algèbre non commutative !
=== propriétés de la Q-excentricité ===
Toujours en faisant les calculs d'opérateurs,
on retrouve e.L = 0 , L.e = 0 , e.H = H.e (donc e est bon nombre quantique , et donc dans un sous-ev de la valeur propre de H , e sera stable).
<div style="text-align: center;"><math>e^2 -1 = -(H/E_o)\cdot [L^2/\hbar^2 + 1]</math></div>
Là encore un terme (+1) vient subrepticement se glisser dans les calculs (on a pris Eo = -13.6eV):
Et [e^2,Lz]=0
Mais alors ,dans l'ECOC [H, L², Lz], e² est un bon nombre quantique, et sa valeur est, dans le niveau n :
<div style="text-align: center;">e² = 1 -1/n² -l(l+1)/n²</div>
et par conséquent l ne peut dépasser n-1 ;
Mais on n'attendait pas cette bizarre formule !
=== Boost et Q-excentricité ===
*Et maintenant, la RÉVÉLATION pour tous ceux qui ont fait de la relativité restreinte :
Multiplions le vecteur excentricité par \hbar pour lui donner l'unité d'un moment cinétique et par n par pure commodité dans les calculs.
Nous appellerons ce vecteur <math>\hat{\vec E}</math> ,le vecteur excentricité-boost , qui est un vecteur polaire et non axial.
E commute avec L² , mais pas avec Lz ; et E² est un bon nombre quantique dans l'ECOC [H,L²,Lz], '''mais pas E''' !
MAIS, dans le sous-ev de la couche n ,
<math>[F_{\lambda \mu},F_{\mu \nu}] = F_{\lambda \nu}</math>
où le tenseur antisymétrique 4-4, F est :
(0,E) en première ligne et la matrice 3-3 antisymétrique correspondant à L^ .
VOILA ! l'atome d'hydrogène est invariant par SO(4) [ évidemment pour les états d'énergie positive, par SO(3,1) c'est à dire le groupe de Lorentz ! d'où l'idée de la notation excentricité-boost ! ]: cela était connu de Pauli , de Fock , de Bargmann , etc. Mais à l'époque, peu connaissaient aussi bien que Pauli la relativité restreinte !
Pour démontrer ces relations, il vaut mieux avoir qq notions d'algèbre de Lie (et des formules de trigonométrie correspondantes), car sinon cela peut être un peu long (11 pages dans le X ; et une page dans le Y : X et Y par courtoisie).
=== opérateurs S et D, valeurs propres de H ===
Il "suffit" maintenant de se rendre compte que [H, Lz, Ez] forme un ECOC ( ce qui correspond en mécanique classique aux coordonnées paraboliques et à la vision spinorielle :
soit 2S = L + E et 2D = L- E ;
Alors S² - D² = 0
S et D sont deux moments cinétiques de carrés égaux : s(s+1)
et :
<div style="text-align: center;">
{| border=0
|-----
|
{| border=1 cellpadding=10 style="border-collapse:collapse"
|-----
| <math> [S^2 + D^2 + \hbar^2]\hat{H} = E_o \cdot \hbar^2</math>
|}
|
| |
|}</div>
C'EST FINI : H a pour valeurs propres : E_o/n² avec 4s(s+1) +1 = n²
soit n = s+s+1 , donc de dégénérescence : n² (faire ce petit calcul !).
Voici comment depuis 1926, on eût pu enseigner l'atome d'hydrogène de Pauli (Nobel en 1945 après Heisenberg, Schrödinger et Dirac en 1933).
Pourquoi cela ne s'est-il pas produit ? Vraisemblablement parce que les orbitales paraboliques étaient moins utiles que les orbitales- harmoniques sphériques qui privilégiaient donc l'ecoc [H,L²,Lz].
<!--
== Voir aussi ==
* [[atome]]
* [[atome d'hydrogène]]
* [[Théorie de Schrödinger de l'atome d'hydrogène]]
* [[atome à N électrons]]
* [[Classification périodique]]
----
-->
== Compléments sur SO(6)et SO(4,2) ==
vacances closes après avoir vendu mes merguez, je fais le point sur SO(6). Cf Oliver.
SO(6) comporte <math>C_6^2</math> = 15 générateurs de rotation.
(P. Kustaanheimo and E. Stiefel, J. Reine Angew. Math. 218, 204 (1965). )
la transformation K-S amène l'eq de Schrodinger sous une forme simple :
multiplions tout par r :
<math>-1/2 r \Delta +1 = E\cdot r</math>
et opérons le changement de variables ; il vient :
<math>[L_{56}+ L_{46}-2E \cdot (L_{56}- L_{46})-1]|\psi>=0</math>
En utilisant le "tilt" usuel A , tel que -2E = exp2A et les relations de commutation avec L(45) , l'équation se réécrit :
<math>[e^{i A L_{45}}L_{56}e^{-i A L_{45}}-e^{-A}]|\psi>=0</math>
La solution est immédiate :
les vecteurs propres de L(56) sont <math> \ |\phi_n></math> de valeur propres n= 1,2,3,... et donc A = - ln n et on en tire
<math> \ E_n = -1/2n^2</math> ,
puis en opérant la transformation réciproque de K-S , on retrouve les états propres paraboliques <math> \ |n_1,n_2,m></math>, puis via les symboles 3j-de-Wigner , les états sphériques <math>\ |n,l,m></math> (Kleinert p 964):
Que tout cela paraît naïvement facile! Néanmoins rappelons que Feynman avait calé sur ce problème et que le déblocage de situation s'effectua de 1967 à 1998.
== Retour ==
*[[Mécanique, enseignée via l'Histoire des Sciences|Mécanique , enseignée via l'Histoire des Sciences]]
[[Catégorie:Mécanique, enseignée via l'Histoire des Sciences (livre)]]
65f2bx9l46eesv9eytpcnl0e243a8c0
Liste de mnémoniques
0
12821
763248
759822
2026-04-08T10:03:15Z
~2026-21748-19
123453
Ajout simple
763248
wikitext
text/x-wiki
Cette page contient une '''liste de [[w:mnémotechnique|mnémotechniques]]''', c’est-à-dire différentes constructions qui facilitent la mémorisation.
Par exemple : afin de retenir beaucoup plus facilement les sept péchés capitaux, une phrase mnémotechnique possible offrant une image plus visuelle regroupant une partie ou la totalité d'un péché :
Par goût, Colette envie l'orgue luxueux d'Avarice
(Paresse, gourmandise, colère, envie, orgueil, luxure, avarice)
== Atmosphère ==
=== Structure verticale ===
L’atmosphère terrestre présente une structure verticale en couches basée sur l’évolution de la température. On distingue la Troposphère (siège des phénomènes météorologiques), la Stratosphère, la Mésosphère et la Thermosphère.
'''T'''out '''S'''ur '''M'''a '''T'''ête
== Mathématiques ==
=== Les 126 premières décimales du nombre π (pi) ===
Le nombre de lettres de chaque mot de ce poème correspond à une décimale de [[w:Pi|Pi]]. Un mot de dix lettres correspond au chiffre 0.
{|
|- align="center"
| ''Que'' || || ''j'' || ''<nowiki>’</nowiki>'' || ''aime'' || ''à'' || ''faire'' || ''apprendre'' || ''un'' || ''nombre'' || ''utile'' || ''aux'' || ''sages'' || ''!''
|- align="center"
! 3 !! , !! 1 !! !! 4 !! 1 !! 5 !! 9 !! 2 !! 6 !! 5 !! 3 !! 5
|}
{|
|- align="center"
| ''Immortel'' || ''Archimède'' || '','' || ''artiste'' || ''ingénieur'' || '',''
|- align="center"
! 8 !! 9 !! !! 7 !! 9
|}
{|
|- align="center"
| ''Qui'' || ''de'' || ''ton'' || ''jugement'' || ''peut'' || ''priser'' || ''la'' || ''valeur'' || ''?''
|- align="center"
! 3 !! 2 !! 3 !! 8 !! 4 !! 6 !! 2 !! 6
|}
{|
|- align="center"
| ''Pour'' || ''moi'' || '','' || ''ton'' || ''problème'' || ''eut'' || ''de'' || ''pareils'' || ''avantages'' || ''...''
|- align="center"
! 4 !! 3 !! !!3!! 8 !! 3 !! 2 !! 7 !! 9
|}
{|
|- align="center"
| ''Jadis'' || '','' || ''mystérieux'' || , || ''un'' || ''problème'' || ''bloquait''
|- align="center"
! 5 !! !! 0 !! !! 2 !! 8 !! 8
|}
{|
|- align="center"
| ''Tout'' || ''l'' || ''<nowiki>’</nowiki>'' || ''admirable'' || ''procédé'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''œuvre'' || ''grandiose''
|- align="center"
! 4 !! 1 !! !! 9 !! 7 !! !! 1 !! !! 6 !! 9
|}
{|
|- align="center"
| ''Que'' || ''Pythagore'' || ''découvrit'' || ''aux'' || ''anciens'' || ''Grecs'' ||''.''
|- align="center"
! 3 !! 9 !! 9 !! 3 !! 7 !! 5
|}
{|
|- align="center"
| ''Ô'' || ''quadrature'' || ''!'' || ''Vieux'' || ''tourment'' || ''du'' || ''philosophe'' || ''...''
|- align="center"
! 1 !! 0 !! !! 5 !! 8 !! 2 !! 0
|}
{|
|- align="center"
| ''Insoluble'' || ''rondeur'' || '','' || ''trop'' || ''longtemps'' || ''vous'' || ''avez''
|- align="center"
! 9 !! 7 !! !! 4 !! 9 !! 4 !! 4
|}
{|
|- align="center"
| ''Défié'' || ''Pythagore'' || ''et'' || ''ses'' || ''imitateurs'' || ''.''
|- align="center"
! 5 !! 9 !! 2 !! 3 !! 0
|}
{|
|- align="center"
| ''Comment'' || ''intégrer'' || ''l'' || ''<nowiki>’</nowiki>'' || ''espace'' || ''plan'' || ''circulaire'' || ''?''
|- align="center"
! 7 !! 8 !! 1 !! !! 6 !! 4 !! 0
|}
{|
|- align="center"
| ''Former'' || ''un'' || ''triangle'' || ''auquel'' || ''il'' || ''équivaudra'' || ''?''
|- align="center"
! 6 !! 2 !! 8 !! 6 !! 2 !! 0
|}
{|
|- align="center"
| ''Nouvelle'' || ''invention'' || '':'' || ''Archimède'' || ''inscrira''
|- align="center"
! 8 !! 9 !! !! 9 !! 8
|}
{|
|- align="center"
| ''Dedans'' || ''un'' || ''hexagone'' || '';'' || ''appréciera'' || ''son'' || ''aire''
|- align="center"
! 6 !! 2 !! 8 !! !! 0 !! 3 !! 4
|}
{|
|- align="center"
|''Fonction'' || ''du'' || ''rayon'' || ''.'' || ''Pas'' || ''trop'' || ''ne'' || ''s'' || ''<nowiki>’</nowiki>'' || ''y'' || ''tiendra''
|- align="center"
! 8 !! 2 !! 5 !! !! 3 !! 4 !!2 !! 1 !! !! 1 !! 7
|}
{|
|- align="center"
|''Dédoublera'' || ''chaque'' || ''élément'' || ''antérieur''
|- align="center"
! 0 !! 6 !! 7 !! 9
|}
{|
|- align="center"
|''Toujours'' || ''de'' || ''l'' || ''<nowiki>’</nowiki>'' || ''orbe'' || ''calculé'''e'''''¹ || ''approchera''
|- align="center"
! 8 !! 2 !! 1 !! !! 4 !! 8 !! 0
|}
{|
|- align="center"
|''Définira'' || ''limite'' || '';'' || ''enfin'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''arc'' || '','' || ''le'' || ''limiteur''
|- align="center"
! 8 !! 6 !! !! 5 !! !! 1 !! !! 3 !! !! 2 !! 8
|}
{|
|- align="center"
|''De'' || ''cet'' || ''inquiétant'' || ''cercle'' || '','' || ''ennemi'' || ''trop'' || ''rebelle''
|- align="center"
! 2 !! 3 !! 0 !! 6 !! !! 6 !! 4 !! 7
|}
{|
|- align="center"
|''Professeur'', || ''enseignez'' || ''son'' || ''problème'' || ''avec'' || ''zèle'' || ''!''
|- align="center"
! 0 !! 9 !! 3 !! 8 !! 4 !! 4
|}
¹ Le mot orbe est du masculin mais ce ne fut pas toujours le cas, ceci induit à présent une faute d’accord à « ''calculée'' » que l’on peut remplacer par « ''escompté'' » pour conserver le bon nombre de lettres.
=== Premières décimales de l’inverse du nombre π : 1/π ===
La valeur de '''1/π = 0,3183098''' se retient sous la forme d’une phrase historique faisant référence aux trois glorieuses :
{{Citation|« Les 3 journées de 1830 ont renversé 89 [la Révolution de 1789] »}}.
Le score de la finale de la coupe du monde de football 98 a fait '''un surpris''' (1 sur pi), côté Brésil : '''0''', '''3''' (c’est le feu ! = '''18''' = '''''Cher''''' payé ?) ; '''3-0''' ('''''Gard'''''ez en souvenir) '''98'''
=== Trigonométrie ===
Le principe est de ne retenir que la première lettre ou la première syllabe des mots-clés de chaque définition ou théorème :
==== Définitions ====
* « Cosinus = côté Adjacent sur l'Hypoténuse »
* « Sinus = côté Opposé sur l'Hypoténuse »
* « Tangente = côté Opposé sur côté Adjacent »
Une "phrase" permet de se rappeler ces trois définitions à la fois :
'''cah soh toa''' pour « ''casse-toi'' » : '''C'''osinus = '''A'''djacent sur '''H'''ypoténuse ; '''S'''inus = '''O'''pposé sur '''H'''ypoténuse ; '''T'''angente = '''O'''pposé sur '''A'''djacent. Certains préfèrent '''soh cah toa'''.
On peut aussi ressortir les dénominateurs de chaque fraction (afin de ne pas mélanger numérateurs et dénominateurs dans ces égalités) en apprenant les sons : '''SO-CA-TO, H-H-A''' (HHA étant les dénominateurs : '''S'''in ='''O'''pp /''H''yp , '''C'''os = '''A'''dj/''H''yp, '''T'''an='''O'''pp/''A''dj)
<br />
D'autres méthodes consistent à associer un "mot" facile à retenir à chacune des trois définitions:<br />
- Cosadi - Sinopi - Tanopad <br />
- cosadjip - sinopip - tangopaj <br />
- CAHier - SOHo - TOAst (ou COCA)
==== Théorèmes ====
* « sin (a+b) = sin a cos b + cos a sin b » devient « ''sico cosi'' »
* « cos (a+b) = cos a cos b - sin a sin b » devient « ''coco sisi'' » (ou « ''coco MOINS sisi'' ou « ''coco ISsi'' » pour retenir le signe)
* À noter que la formule « ''sico cosi / coco moins sisi'' » ou « ''Coco si méchant, si, Coco, si'' » permet également d’apprendre les formules de factorisation suivantes :
sin p + sin q = 2 sin [(p+q)/2] •cos [(p-q)/2]
sin p - sin q = 2 cos [(p+q)/2] •sin [(p-q)/2]
cos p + cos q = 2 cos [(p+q)/2] •cos [(p-q)/2]
cos p - cos q = -2 sin [(p+q)/2] •sin [(p-q)/2]
Avec p = A + B et q = A - B
'''Cosinus est menteur et raciste''' ('''CO'''s comme '''CO'''n) en effet cos (a+b) donne (cos a cos b) - (sin a sin b). Cosinus est donc menteur puisque le signe de l’addition (positive) est négatif. Cosinus est raciste puisque on obtient (cos a cos b) d’une part et (sin a sin b) d’autre part : les cosinus et les sinus ne se mélangent pas.
'''co'''sinus est un '''co'''pain '''co'''n (copain pour le sens et con pour le signe): cos(a'''+'''b)= cos(a)cos(b) '''-''' sin(a)sin(b) : les cosinus restent ensemble, mais le signe change.
'''s'''inus est une '''s'''alade '''s'''ympa (salade pour le sens et sympa pour le signe): sin (a'''+'''b)=sin(a)cos(b) '''+''' sin(b)cos(a) : sin et cos se mélangent mais le signe reste le même
On trouve également : '''opip adjip opadj''' : sinus ('''op'''posé sur '''hyp'''oténuse), cosinus ('''adj'''acent sur '''hyp'''oténuse), tangente ('''op'''posé sur '''adj'''acent). La phrase prononcée rapidement d’un seul coup est très facile à mémoriser.
De même que '''SOH CAH TOA''': '''S'''inus= '''O'''pposé sur '''H'''ypoténuse '''C'''osinus= '''A'''djacent sur '''H'''ypoténuse '''T'''angente= '''O'''pposé sur '''A'''djacent
On peut lui substituer la formule plus percutante : '''CAH SOH TOA''' (à prononcer Casse toi ! )
=== Dates et constantes ===
Le [[w:code chiffres-sons|code chiffres-sons]] est une méthode qui permet de se souvenir de dates ou de valeurs numériques en formant des phrases.
=== Formules de géométrie ===
* Circonférence d’un cercle : 2 pi R (2 pierres)
** La circonférence est toute fière d’être égale à 2 pi R
* Aire d’un disque: pi R<sup>2</sup> (« pierre carrée » ou « pierre deux »)
** Le cercle est tout joyeux d’être égal à pi R<sup>2</sup> (prononcer « pi R deux »)
* « Le volume de la sphère, est quoi qu’on y puisse faire, 4/3 pi R<sup>3</sup>, fut-elle de bois. » ([[w:fr:Marcel Pagnol|Marcel Pagnol]])
Le volume d'une sphère, qu'elle soit de pierre, qu'elle soit de bois est égal aux 4/3 de pi R3
* Le volume d'une pizza (d'un camembert, ou de n'importe quel objet semblable) de rayon 'z' et de hauteur 'a' est égale à '''Pi.(z.z).a''' (la formule correspond à son nom)
soit V = π.z<sup>2</sup>.a
=== Analyse vectorielle ===
[[File:DRG chart fr.svg|thumb|right|300px|Diagramme des principales relations entre opérateurs de calcul vectoriel.]]
* Opérateurs s'annulant: '''DiR'''i'''G'''é (décrivant les flèches centrales sur le diagramme à droite)
** '''DiR'''i: <math>\mathrm{div}(\overrightarrow{\mathrm{rot}})=0</math>
** '''R'''i'''G''': <math>\overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{grad}})=\vec{0}</math>
* Autres formules (flèches reliant div et grad sur le diagramme à droite):
** <math>\Delta = \mathrm{div}(\overrightarrow{\mathrm{grad}})</math>
** <math>\overrightarrow{\mathrm{grad}}(\mathrm{div})= \overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{rot}})+\vec{\Delta}</math>
=== Ordre des opérations ===
En algèbre, les opérations simples : <code>(</code> <code>)</code>, <code>+</code>, <code>-</code>, <code>×</code> et <code>÷</code>, sont évaluées selon un certain ordre : '''PEMDAS''' pour « '''p'''arenthèses, '''e'''xposant,
'''m'''ultiplication, '''d'''ivision, '''a'''ddition et '''s'''oustraction ». Pour plus de détails sur l'application de ce mnémonique, voir [[:w:fr:Ordre des opérations|Ordre des opérations]].
=== Double distributivité ===
Retenir le mot « '''PIED''' » qui donne les termes à regrouper lorsque l’on développe : '''P'''remiers, '''I'''ntérieurs, '''E'''xtérieurs, '''D'''erniers.
=== Constante e<ref name="villemin.gerard">http://villemin.gerard.free.fr/Wwwgvmm/MnemoTe/Phrase.htm</ref> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Tu
|
|align="center"|aideras
|
|align="center"|à
|
|align="center"|rappeler
|
|align="center"|ta
|
|align="center"|quantité
|
|align="center"|à
|
|align="center"|beaucoup
|
|align="center"|de
|
|align="center"|docteurs
|
|align="center"|amis.
|-
|align="center"|2
| ,
|align="center"|7
|
|align="center"|1
|
|align="center"|8
|
| align="center" |2
|
|align="center"|8
|
|align="center"|1
|
|align="center"|8
|
|align="center"|2
|
|align="center"|8
|
|align="center"|4
|}
=== Nombre d'or<ref name="villemin.gerard"/> ===
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|Ô
|
|align="center"|nombre
|
|align="center"|d'
|
|align="center"|élégance
|
|align="center"|!
|
|align="center"|Toi,
|
|align="center"|toi,
|
|align="center"|grandiose,
|
|align="center"|étonnant :
|
|align="center"|''le nombre d'or''.
|-
|align="center"|1
| ,
|align="center"|6
|
|align="center"|1
|
|align="center"|8
|
|align="center"|0
|
|align="center"|3
|
|align="center"|3
|
|align="center"|9
|
|align="center"|8
|
|align="center"|
|}
(! pour 0)
=== Statistiques ===
* [[w:Règle 68-95-99.7|Règle 68-95-99.7]] : la proportion des échantillons entre [-σ, +σ], [-2σ, +2σ], [-3σ, +3σ] pour une distribution gaussienne centrée.
* Erreurs de première espèce et deuxième espèce.
** Se rappeler la fable d’Ésope dans laquelle un enfant [[wikt:crier au loup|crie au loup]] (hypothèse nulle <math>H_0</math>: « il n'y a aucun loup »).
**# D'abord, les villageois pensent qu'il y a un loup alors qu'il n'y en a aucun (erreur de première espèce).
**# Puis, les villageois pensent qu'il n'y a aucun loup alors qu'il y en a un (erreur de seconde espèce).
** Il y a une barre dans '''P'''ositif (faux positif : erreur de type '''I''') et deux barres dans '''N'''égatif (faux négatif : erreur de type '''II''').
== Sciences ==
=== Astronomie ===
==== Ordre des planètes du Système solaire ====
Il existe toute une série de termes mnémotechniques pour se souvenir de l'ordre des planètes à l’intérieur du [[w:système solaire|Système solaire]]. La première lettre de chaque mot de cette phrase correspond à la première lettre de chaque [[w:planète|planète]], de la plus rapprochée à la plus éloignée du Soleil. L'[[w:apostrophe|apostrophe]] ou la [[w:virgule|virgule]] peut représenter la [[w:ceinture d'astéroïdes|ceinture d'astéroïdes]] entre [[w:Mars (planète)|Mars]] et [[w:Jupiter|Jupiter]].
Voici l’ordre des planètes du Système solaire :
Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.
''À NOTER que selon la [[w:Définition des planètes de l'UAI|nouvelle définition]] de l’[[w:Union astronomique internationale|Union astronomique internationale]] d’août [[w:2006|2006]], [[w:Pluton (planète naine)|Pluton]] n’est plus considérée comme une [[w:planète|planète]] mais comme une planète naine ([[w:(134340) Pluton|(134340) Pluton]])'' (de même que [[w:(1) Cérès|(1) Cérès]], [[w:(136199) Éris|(136199) Éris]], [[w:(136108) Haumea|(136108) Haumea]] et [[w:(136472) Makemake|(136472) Makemake]]), <br/>
'''''Ordre des planètes du Système solaire : '''Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.''
On emploie par exemple les phrases suivantes :
*'''''M'''ême '''V'''ieux '''T'''ruc '''M'''ais '''J''''en '''S'''ais '''U'''n '''N'''ouveau.''
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arie a '''J'''eté '''S'''amedi '''U'''n '''N'''avet.''
*'''''M'''a '''V'''ieille '''T'''rompette '''M'''e '''J'''oue '''S'''on '''U'''ltime '''N'''octurne.''
*'''''M'''a '''V'''oiture '''T'''e '''M'''ène '''J'''oyeusement '''S'''ur '''U'''ne '''N'''ationale.''
*'''''M'''arie, '''V'''iendras-'''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe ?''
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe .''
*'''''M'''e '''V'''oici '''T'''oute '''M'''ignonne''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ébuleuse.''
*'''''M'''e '''V'''oici '''T'''oute '''M'''odifiée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ouveauté.''
*'''''M'''e '''V'''oilà '''T'''out '''M'''ouillé''',''' '''J''''ai '''S'''uivi '''U'''n '''N'''uage.''
*'''''M'''e '''V'''oilà '''T'''oute '''M'''ouillée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ymphomane.''
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''avette.''
*'''''M'''on '''V'''ioloncelle '''T'''ombe, '''M'''ais '''J'''e '''S'''auve '''U'''ne '''N'''ote.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours !''
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur.''
*'''''M'''a '''V'''ille '''T'''hionville '''M'''ontre '''J'''oyeusement '''S'''on '''U'''nivers '''N'''octurne.''
*'''''M'''on '''V'''élo '''T'''e '''M'''ènera ''' J'''usque '''S'''ur '''U'''n '''N'''uage.''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al, ''' J''''en '''S'''ouhaite '''U'''n '''N'''ouveau.''
*'''''M'''onsieur, '''V'''ous '''T'''ravaillez '''M'''al ; - ''' J'''e '''S'''uis '''U'''n '''N'''ovice.''
*'''''M'''al '''V'''êtu '''T'''oi '''M'''ême, '''J'''e '''S'''uis '''U'''n '''N'''udiste''
*'''''M'''on '''V'''ieux '''T'''outou '''M'''édor '''J'''oue '''S'''ur '''U'''n '''N'''uage''
*'''''M'''a '''V'''erge '''T'''e '''M'''ènera '''J'''usque '''S'''ur '''U'''n '''N'''uage''
*'''''M'''ais '''V'''ous '''T'''ombez '''M'''al, '''J''''ai '''S'''auté '''U'''ne '''N'''aine''
* '''''M'''essieurs!''' V'''otre '''T'''rahison '''M'''<nowiki/><nowiki>'écœure: </nowiki>'''J'''ouer''' S'''ur '''U'''ne '''N'''omenclature!'' (au sujet de la disparition de Pluton de la liste)
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe.''
*'''''M'''arquez '''V'''otre '''T'''emps '''M'''esuré '''J'''uste '''S'''ous '''U'''ne '''N'''anoseconde.''
'''Et pour les nostalgiques de Pluton...'''
* '''''M'''anon '''V'''iendras '''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''ercure '''V'''eut '''T'''aquiner '''M'''ars, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''ademoiselle, '''V'''ous '''T'''ravaillez '''M'''al, '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''rofesseur''
*''Le '''M'''onde '''V'''oit '''T'''ourner du '''M'''atin '''J'''usqu'au '''S'''oir '''U'''niquement '''N'''euf '''P'''lanètes''.
*'''''M'''on '''V'''ieux, '''T'''u '''M'''e '''J'''ettes '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.''
*'''''M'''onsieur '''V'''euillez '''T'''ournez '''M'''a '''J'''upe '''S'''ans '''U'''ne '''N'''aïve '''P'''udeur.''
*'''''M'''e '''V'''oici, '''T'''oute '''M'''ignonne, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''rofessionnel.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''ressé.
*'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J''''ai '''S'''uivi '''U'''n '''N'''uage '''P'''luvieux.
*'''''M'''ais '''V'''iendras-'''T'''u '''M'''anger, '''J'''ulie, '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.
*'''''M'''on '''V'''ieux '''T'''héâtre '''M'''e '''J'''oue '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce.
*'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.''
*'''''M'''onsieur '''V'''ous '''T'''irez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ovice '''P'''itoyable.
* '''''Mé'''lanie, '''V'''ous '''T'''ombez '''Ma'''l, '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.''
*'''''M'''on '''V'''aisseau '''T'''e '''M'''ènera '''J'''eudi '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.
*'''''M'''a '''V'''ieille '''T'''ante '''M'''arge '''J'''oue '''S'''ur '''U'''n '''N'''ouveau '''P'''iano.
*'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours '''P'''ourri !''
*'''''M'''arin '''V'''aleureux, '''T'''u '''M'''ourras '''J'''eune '''S'''ur '''U'''n '''N'''avire '''P'''erdu !''
*'''''M'''on '''V'''élo '''T'''ourne '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''iéton.
*'''''M'''erci '''V'''ous '''T'''ous '''M'''aintenant '''J'''e '''S'''ais '''U'''nir '''N'''euf '''P'''lanètes.
*'''''M'''élanie '''V'''ous '''T'''ombez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.
*'''''M'''es '''V'''ieilles '''T'''antes '''M'''angeaient '''J'''adis '''S'''ur '''U'''ne '''N'''appe '''P'''ercée.''
*'''''M'''ets '''V'''ite '''T'''on '''M'''aillot '''J'''e '''S'''uis '''U'''n '''N'''udiste '''P'''oilu.
* '''''M'''on '''V'''ieux '''T'''acot '''M'''´a '''J'''eté '''S'''ur '''U'''n '''N'''oble '''P'''''assant.
*'''''M'''ange '''V'''ite '''T'''on '''M'''ars '''J''' 'en '''S'''ors '''U'''n '''N'''ouveau '''P'''aquet.
*'''''MÈR'''E, '''V'''iens '''Ter'''miner '''M'''a '''JUP'''e, '''SA''' cout'''UR'''e '''NE''' tient '''PLU'''s.''
* '''''M'''aman, '''V'''oudrais-'''T'''u '''M''''emmener '''J'''ouer '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète ?''
Suite à un concours qui s’est déroulé au Québec, la formule suivante a été retenue :
*'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe !''
Il existe aussi cette formule (la plus ancienne mnémonique connue en astronomie) qui se retient facilement, grâce à ses trois mots de trois syllabes :
*''Merveter, Marjusa, Uneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Mar'''s, '''Ju'''piter, '''Sa'''turne, '''U'''ranus, '''Nep'''tune, '''Plu'''ton)
Une variante<ref>Formule tirée de l’un des tomes du [https://fr.m.wikipedia.org/wiki/Manuel_des_Castors_Juniors ''Manuel des Castors Juniors'']</ref> de celle-ci :
* ''Mervé'', ''Termaju'', ''Saturneplu''
('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Ma'''rs, '''Ju'''piter, '''Sat'''urne, '''Ur'''anus, '''Ne'''ptune, '''Plu'''ton)
Qui existe aussi sous cette forme :
* ''Mervé, Termaju, Satur n'est plus''
Et celle-ci qui inclut le Soleil :
*'''''S'''alut ! '''Me''' '''v'''ois-'''t'''u ? '''M'''oi '''j'''e '''s'''uis '''u'''ne '''n'''ouvelle '''pl'''anète !''
'''Planète ayant un système d'anneaux'''
* '''J'''e '''S'''uis '''U'''ne '''N'''ouille (Jupiter, Saturne, Uranus, Neptune)
==== Ordre des quatre lunes principales de Jupiter ====
'''I'''l '''e'''st '''g'''rand, '''C'''harles !
* [[w:Io|Io]], [[w:Europe|Europe]], [[w:Ganymède_(lune)|Ganymède]], [[w:Callisto_(lune)|Callisto]]
==== Croissant de Lune ====
Le '''p'''remier croissant et le '''d'''ernier croissant peuvent être reconnus en les assimilant aux sens du p et du d. En effet, en « ajoutant » au croissant de lune un bâton, on obtient un p ou un d selon le croissant. Cette méthode marche uniquement dans l'[[w:Hémisphère (géographie)|hémisphère]] [[w:nord|nord]], dans l’hémisphère sud il faudra considérer que la Lune ment.
Une méthode plus simpliste consistait autrefois à lire le croissant de lune directement. Quand il formait un '''C''' la lune incitait à penser qu'elle était '''C'''roissante . Or dans ce cas là elle est décroissante. Et quand elle formait un '''D''' (en supposant l’ajout de la barre droite nécessaire) elle incitait à penser qu’elle était '''D'''écroissante. Or dans ce cas là elle est croissante. Il en est venu l’expression populaire : ''Il est menteur comme la lune''. Cependant, dans ce cas la Lune ne ment que dans l'hémisphère Nord : C correspond bien à la Lune croissante et D à la Lune décroissante.
Ces méthodes ne sont plus valables autour de l’[[w:Équateur (ligne équinoxiale)|équateur]], ou le sens de ''lecture'' varie selon les saisons.
==== [[w:type spectral|Types spectraux]] [[w:étoile|stellaires]] ====
Les différents [[w:type spectral|types spectraux]], du plus chaud au plus froid, sont : O, B, A, F, G, K, M.
'''''O'''h, '''b'''e '''a''' '''f'''ine '''g'''irl/'''g'''uy, '''k'''iss '''m'''e !''
'''''O'''verseas '''b'''roadcast: '''a''' '''f'''lash! [[w:Godzilla|'''G'''odzilla]] '''k'''ills [[w:Mothra|'''M'''othra]] !''
=== Physique ===
==== Électromagnétique ====
Énergie électrique stockée dans un condensateur :
<math>E= (1/2) C U^2</math>
"l'''e''' '''demi''' '''cu'''l '''carré'''"
==== Les sept unités fondamentales====
Pour: ''seconde, ampère, candela, kilogramme, mètre, kelvin, mole '':
<br> Sac km km
<br> Je <u>'''s'''ais q</u>u<u>'''a'''n</u>d <u>'''c'''a</u>c<u>'''k'''i</u> <u>'''m'''et</u> <u>'''k'''el</u> <u>'''m'''o</u>t ! : (je) ''Sec-Am-Ca-Ki-Mè-Kel-Mo''
<br> <u>Ce con</u> d'<U>Ampère</U>, <u>qu'en d</u>it <u>qui l'au</u>ra!, <u>mettr</u>a <u>quel vin</u> au <u>môl</u>e ?: ''Sec<s>onde</s>'' d' ''ampère'', ''cand<s>ela</s>'' ''kilo<s>gramme</s>''ra, ''mètr<s>e</s>''a ''kelvin'' au ''moles''
"Secondes molles, quand des lacs-îlots [[wikt:grammer|grammant]] [[wikt:pairer|pairent]], Maître Kelvin !" (qui se prononce comme : "seconde, mole, candela, kilogramme, ampère, mètre, kelvin")
*
==== Ordre des couleurs de [[w:Résistance (composant)#Repérage et valeurs normalisées|résistance électrique]] ====
('''N'''oir, '''M'''arron, '''R'''ouge, '''O'''range, '''J'''aune, '''V'''ert, '''B'''leu, '''Vio'''let, '''G'''ris, '''B'''lanc)
'''''N'''e '''M'''ange '''R'''ien '''O'''u '''J'''e '''V'''ais '''B'''leuir '''V'''iolemment (ton) '''G'''ros '''B'''laze.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rule '''V'''otre '''G'''rosse '''B'''arbe.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''eûnez, '''V'''oilà '''B'''ien '''V'''otre '''G'''rande '''B'''êtise.''
'''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rise '''V'''otre '''G'''rosse '''B'''outeille.''
'''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''ats '''V'''iolemment '''G'''ros '''B'''êta
'''''N'''adine '''M'''e '''R'''épondit''' O'''ui, '''J'''e '''V'''eux '''B'''ien '''V'''otre '''G'''rosse '''B'''iroute''
'''''N'''oir, '''M'''arron, les couleurs de l'arc en ciel (sauf l'indigo), '''G'''ris, '''B'''lanc.''
Version québécoise utilisant la lettre '''B''' pour Brun au lieu de Marron :
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ière et '''V'''in '''G'''ratuit, '''B'''ienvenue.''
'''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ien'''V'''enue '''G'''ros '''B'''uveur.''
==== Ordre des couleurs du [[w:Couleur#Le spectre lumineux|spectre visible]] ====
Les sept couleurs du spectre visible ou de l'arc-en-ciel
(dans l'ordre des fréquences croissantes : '''R'''ouge - '''O'''range - '''J'''aune - '''V'''ert - '''B'''leu - '''I'''ndigo - '''V'''iolet)
peuvent se retenir grâce à la phrase suivante :
La '''ROU'''sse '''OR'''ienta le '''J'''uge '''VER'''s le '''BL'''azer de l''''INDI'''enne '''VIOL'''ée.
Dans l'ordre inverse (soit de la plus petite à la la plus grande longueur d'onde) elles peuvent se retenir grâce au mot '''VIBUJOR''', en remplaçant le '''U''' par un '''V''' ('''vert''' comme '''Hu'''lk).
'''V'''iolet - '''I'''ndigo - '''B'''leu - '''V'''ert - '''J'''aune - '''O'''range - '''R'''ouge
Remarque : en se figurant le drapeau français '''bleu'''-'''blanc'''-'''rouge''', on peut retrouver l’ordre des longueurs d’onde, en assimilant le bleu à l’ultraviolet, le blanc au visible, et le rouge à l’infra-rouge : '''ultraviolet'''-'''visible'''-'''infra-rouge'''.
==== Longueur d'onde des couleurs ====
Le mot rouge est plus long que le mot bleu (5 vs 4), sa longueur d'onde est plus longue également.
==== Couleurs en peinture et rayonnements lumineux ====
Les peintres utilisent les trois couleurs fondamentales '''Cyan Magenta et Jaune''', chacune absorbant une seule des trois couleurs fondamentales de la lumière (Rouge Vert et Bleu). Notre œil ne reconnaît la couleur que par la lumière identifiée par chacune des 3 familles de cônes de l'œil respectivement sensibles aux rayonnements '''Rouge Vert et Bleu'''.
Cette phrase permet aux peintres et aux physiciens d'identifier un équivalent des deux couleurs de rayonnement lumineux identifiées par les cônes de l'œil pour chacune des couleurs fondamentales de la peinture.
'''C'''ette '''B'''onne '''V'''ieille<br />
'''M'''ijote des '''R'''aviolis "'''B'''uitoni"<br />
'''<nowiki>J'</nowiki>'''en '''R'''é'''V'''ais
La couleur '''C'''yan de la peinture correspond ainsi à la réception des rayonnements lumineux '''B'''leu et '''V'''ert.
Le '''M'''agenta, pour sa part correspond aux rayonnements lumineux '''R'''ouge et '''B'''leu.
Quant à la couleur '''J'''aune, elle renvoie vers l'œil les rayonnements lumineux '''R'''ouge et '''V'''ert.
==== Constantes ====
* vitesse de la lumière<ref name="villemin.gerard" /> :
:{| style="text-align: center;"
|Ah,||messagère||admirable,||lumière||éclatante,||je||sais||votre||célérité||
|-
|La||constante||lumineuse||restera||désormais||là||dans||votre||cervelle||
|-
|2||9||9||7||9||2||4||5||8||m/s
|}
* définition formelle d'une seconde (périodes de la radiation correspondant à une transition entre les deux sous-niveaux hyperfins du césium 133) :
:{| style="text-align: center;"
|« Pharaonne,||j'||affirmais||là,||honore||mal||l'||aimable||seconde||0 ! »
|-
|9||1||9||2||6||3||1||7||7||0
|}
=== Chimie ===
'''Priorité des groupes caractéristiques en nomenclature'''
Pour nommer une molécule composée de plusieurs groupes caractéristiques, on utilise l'ordre suivant :
Acide carboxylique - anhydride d'acide - ester - halogénure d'acyle - amide - nitrile - aldéhyde - cétone - alcool - amine - alcyne - alcène - éther-oxyde - dérivé halogéné - alcane
Pour se rappeler de l'ordre :
'''Ac'''e '''an'''nule '''Ester''' ! '''Halo ami'''? '''Ni'''e '''al'''ors '''Cé'''cile '''alcool'''isée, '''Am'''élie '''ascene''' à N'''eoxi''' et '''De'''lp'''h'''ine un '''alcane'''
==== Radicaux alkyles ====
Pour se rappeler l’ordre des 3 premiers groupement alcanes :
* Il ai'''mait''' '''êt'''re '''pro'''pre. (Oralement, "Il ai'''Mét Éth Prop''' ")
Pour se rappeler l’ordre des 4 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane)
* '''M'''aman '''Et''' '''P'''apa '''B'''ébé
* '''M'''aman '''Et''' '''P'''apa '''But'''inent.
* '''M'''alin qui '''É'''tudie '''P'''our le '''B'''ac.
* '''M'''ieux '''É'''tudier '''P'''our le '''B'''ac.
* '''M'''on '''É'''cole '''P'''eut '''B'''rûler.
* '''M'''on '''É'''lève '''P'''isse '''B'''ien
* '''M'''organe '''E'''st '''P'''as '''B'''elle.
* '''Me'''s '''é'''lèves '''p'''arlent '''b'''eaucoup.
* '''M'''ets '''t'''es '''Prop'''res '''But'''s ! (Oralement, "Mét Éth Prop But")
Pour se rappeler l’ordre des 5 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane)
* '''M'''aman '''E'''st '''P'''artie '''B'''ébé '''P'''leure
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte
Pour se rappeler l’ordre des 6 premiers groupement alcanes :
* ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane, '''H'''exane)
* '''M'''aurice '''E'''st '''P'''as '''B'''eau '''P'''our '''H'''élène.
* '''M'''amie '''E'''t '''P'''api '''B'''atifolent '''P'''endant l''''H'''iver.
* '''M'''aman '''E'''t '''P'''apa '''B'''oivent '''P'''endant '''H'''alloween
* '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte de '''H'''eineken
* '''M'''et '''E'''th '''P'''rop '''B'''ut '''P'''ent '''H'''ex
Pour les plus vulgaires :
* '''M'''audite '''É'''paisse ! '''P'''ourquoi '''B'''aiser '''P'''our l' '''H'''iver !
==== [[w:Tableau périodique des éléments|Tableau périodique des éléments]] ====
Il est à noter que la plupart des moyens mnémotechniques concernant les éléments ont été créés par des [[w:étudiant|étudiant]]s, d’où le [[w:vocabulaire|vocabulaire]] parfois amusant des maximes.
===== [[w:Éléments de la période 2|Période 2]] =====
'''Pour : Li'''thium, '''Bé'''ryllium, '''B'''ore, '''C'''arbone, '''N'''itrogène (Azote), '''O'''xygène, '''F'''luor, '''Né'''on.''
* «La '''Li''''''Bé'''llule '''B'''leue, d’une '''C'''aresse, '''N'''oit dans l’'''O'''nde la '''F'''leur de '''Né'''nuphare. »
* « '''Li'''thus et '''Be'''rénice '''B'''oivent, '''C'''haque '''N'''uit, '''O''' '''F'''rais de '''Né'''ron »
* « '''Li'''verpool, '''Be'''rceau des '''B'''eatles, '''C'''onnait '''N'''aturellement ces '''O'''librius '''F'''ous et '''Né'''vrosés »
* « '''Li'''bérez '''Be'''n '''B'''arkans, '''C'''élèbre '''N'''arrateur, '''O'''u '''F'''usillez '''Né'''ron »
* « '''Li''' '''Be''' le '''B'''on '''C'''anard du '''N'''ord '''O'''uest de la '''F'''rance '''Ne'''ogauchiste »
* « '''Li'''li '''Be'''sa '''B'''ien '''C'''ouchée '''N'''ue '''O''' '''F'''lanc de '''Né'''ron »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''umeur de '''Ne'''squik »
* « '''Li'''li '''Be'''se '''B'''ien '''C'''onfortablement '''N'''otre '''O'''ncle '''F'''rançois '''Ne'''stor »
* « '''Li'''mace '''Be'''te '''B'''ouffa '''C'''inq '''N'''ouveaux '''O'''ignons '''F'''raîchement '''Né'''s »
* « '''Li'''li '''Be'''rça '''B'''ébé '''C'''hez '''N'''otre '''O'''ncle '''F'''ernand '''N'''estor
* « '''Li'''li '''B'''ecta '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''erdinand '''N'''estor »
* « '''Li'''li '''Bé'''cha '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''Li'''li '''Bé'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br />
* « '''LiBe'''rté '''B'''afouée '''C'''ontre '''N'''otre '''O'''rganisation '''F'''édérale '''Né'''ogaulliste (ou '''Né'''otrotskiste) »
* « le '''Li''' t de '''BE''' '''B''' é a '''C'''assé le '''N'''ez de l' '''O'''ncle '''F'''urieux '''Né'''on »
* « '''Li'''vrez '''Bê'''tement '''B'''ataille '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''L'i'''magination '''Be'''lliqueuse '''B'''aissa '''C'''ar '''N'''otre '''O'''rdre '''F'''ut '''Ne'''t »
* « '''Li'''re '''Be'''aucoup '''B'''alzac '''C'''ar '''N'''otre '''O'''rthographe '''F'''ait '''Né'''gligé »
* « '''Li'''ste de '''Be'''lles '''B'''outeilles de '''C'''ognac '''N'''ous '''O'''nt '''F'''outus '''Ne'''rveux »
* « '''Li'''li '''Be'''cote '''B'''ien '''C'''omme '''Ni'''cole '''O''' '''F'''ond '''N'e'''st ce pas »
* « '''Li'''bérez '''Be'''rnard '''B'''ossu '''C'''ontre '''N'''ouvel '''O'''tage '''F'''éminin. Signé '''Ne'''on »
* « '''LiBe'''rté de '''B'''oire '''C'''ar '''N'''ous '''O'''n '''F'''oire '''N'''os '''e'''xams »
* « '''Li'''bérez '''Be'''n '''B'''arka '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions »
* « '''Li'''li et '''Be'''rnard '''B'''aisent '''C'''omme '''N'''ous '''O'''n '''F'''ait '''Ne'''spa »
===== [[w:Éléments de la période 3|Période 3]] =====
''Pour : '''S'''odium, '''M'''a'''g'''nésium, '''Al'''uminium, '''Si'''licium, '''P'''hosphore, '''S'''oufre, '''C'''h'''l'''ore, '''Ar'''gon.''
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''aquer '''A'''p'''r'''ès. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer des '''Ar'''ticulations. »
* « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets (ou '''P'''erdrix) '''S'''ans '''Cl'''aquer d' '''Ar'''gent. »
Le sodium est représenté par '''Na'''. Alors Napoléon remplace Suzanne pour retrouver le symbole :
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer d''''Ar'''gent » — ou « sans claquer '''A'''p'''r'''ès », « d''''Ar'''gon », « d''''Ar'''tère », « (d')'''Ar'''tiche » ou « sans claquer les '''Ar'''ticulations » pour éviter la confusion avec l'élément argent, noté '''Ag'''.
* « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''ore l’'''Ar'''mistice. »
* « '''Na'''guère '''M'''onsei'''g'''neur '''Al'''louche '''Si''' '''P'''ervers '''S'''uça '''Cl'''aire '''Ar'''demment. »
* « '''Na'''poléon '''M'''an'''g'''eait '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''amser '''A'''p'''r'''ès. »
* « '''Na'''poléon '''M'''a'''g'''nera '''À''' '''l'''<nowiki/>'est '''Si''' '''P'''ossible '''S'''a '''C'''o'''l'''onne '''A'''rmée. »
*« '''Na'''billa '''M'''an'''g'''e (h)'''Al'''lal '''Si''' '''P'''atrick '''S'''ébastien '''Cl'''ash '''Ar'''thur. »
===== [[w:Éléments de la période 4|Période 4]] =====
''Pour : '''K'''allium (Potassium), '''Ca'''lcium, '''Sc'''andium, '''Ti'''tane, '''V'''anadium, '''C'''h'''r'''ome, '''M'''a'''n'''ganèse, '''Fe'''r, '''Co'''balt, '''Ni'''ckel, '''Cu'''ivre, '''Z'''i'''n'''c, '''Ga'''llium, '''Ge'''rmanium, '''A'''r'''s'''enic, '''Sé'''lénium, '''Br'''ome, '''Kr'''ypton.''
* « '''K'''arl '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fé'''conda le '''Co'''n de '''Ni'''cole, et le '''Cu'''l de ses '''Z'''e'''n'''nemies, '''Ga'''rdant '''Ge'''néreusement '''As'''sez de '''Se'''mence pour ce '''Br'''ave '''K'''h'''r'''ouchtchev. »
* « '''K'''évin '''Ca'''pture un '''Sc'''arabée '''Ti'''mide dans le '''V'''agin '''Cr'''éatif de '''M'''o'''n'''ique, '''Fé''' (fait) '''Co'''mme '''Ni'''cole dans le '''Cu'''l en '''Zinc''' de '''Ga'''spard de '''Ge'''rmanie, puis '''As'''pire '''Sé'''bastien dans la '''Br'''aguette du '''Kr'''aken »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''iemment '''Ti'''to. '''V'''orochev '''Cr'''ia '''M'''ag'''n'''anime : "'''Fé''' pas le '''Co'''n '''Ni'''kita, ton '''Cu'''l en '''Z'''i'''n'''c '''Ga'''lvanisé te '''Gè'''ne '''AsSe'''z pour '''Br'''anler des '''Kr'''evettes. »
* « '''K'''épler '''Ca'''lculait des '''Sc'''alaires '''Ti'''tanesques, '''V'''oyant '''Cr'''o-'''M'''ag'''n'''on '''Fé'''sant le '''Co'''n '''Ni'''ché sur le '''Cu'''l d'un '''Z'''ébulo'''n''', '''Ga'''gnant '''Gé'''néralement '''AsSe'''z de '''B'''iè'''r'''es '''Kr'''onenbourg. »
* « '''K''' '''Ca''' (cacas) '''Sc'''iés de '''Ti'''ti '''V'''olant et '''Cr'''os '''M'''i'''n'''et qui '''F'''ont ('''Fe''') des '''Co'''nneries ont '''Ni'''qué le '''Cu'''l à '''Z'''a'''n'''zibar d'un '''Ga'''rs '''Gé'''nial '''As'''sis '''Se'''rrant une '''B'''onne ('''Br''') '''Kr'''o. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement '''Ti'''tov. '''Van'''ia '''Cr'''ia '''M'''ag'''n'''animement "'''F'''ais pas le '''Co'''n Nikita, la Cuisine en '''Z'''i'''n'''c de la '''Ga'''re de '''Ge'''nève '''As''' Ses '''Br'''iques '''Cr'''euses » (ou '''K'''hrouchtchev '''Ca'''ssa le '''SC'''ooter à '''TI'''tov)
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement la '''Ti'''gnasse de '''V'''anadia, '''Cr'''oyant '''M'''a'''n'''ifestement '''Fe'''re '''Co'''cu '''Ni'''colaiev, le '''Cu'''ré de '''Z'''a'''n'''zibar '''Ga'''gna '''Ge'''nève '''As'''sez '''Se'''crètement avec Son '''Br'''éviaire '''Kr'''ipté. »
* « '''K'''arl '''Ca'''valier '''Sc'''andinave '''Ti'''ra '''V'''engeance '''C'''r'''u'''elle '''M'''a'''n'''iant le '''Fe'''r '''Co'''ntre le '''Ni'''kel. Le '''Cu'''l de '''Z'''e'''n'''obie '''Ga'''rnit de '''Ge'''ranium '''As'''pire la '''Se'''ve '''Br'''ûlante du '''Kr'''atère (cratère/Krypton). »
* « '''K'''onrad '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fe'''rmant le '''Co'''n de '''Ni'''cole, le '''Cu'''l de '''Zn'''obie, et '''Ga'''rdant '''Ge'''néreusement l''''As'''permique '''Se'''mence du '''Br'''ave '''K'''e'''r'''mit. »
* « '''K'''a'''Ca''' '''Sc'''quatte avec '''Ti'''ti la '''V'''oiture de '''Cr'''os '''M'''i'''n'''et. '''Fe'''rnand '''Co'''nduit sa mi'''Ni''' '''COOPER''' en '''Z'''i'''g'''za'''Ga'''nt, '''Ge'''néralement '''As'''sis en se '''Se'''rvant une '''B'''ière '''Kr'''onembourg. »
* « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''rupuleusement le '''Ti'''tanesque et '''V'''elue '''Cr'''ane du '''M'''o'''n'''de avec '''Fe'''rmeté. '''Co'''ntre l'ennemie, '''Ni'''kita '''C'''a'''u'''sa la '''Z'''iza'''n'''ie, il en'''Ga-Ge'''a l''''As'''sault '''S'''talinien. '''Br'''avo '''K'''h'''r'''ouchtchev. »
* « '''K'''af'''Ca''' (Kafka) '''Sc'''ruta, '''Ti'''mide, '''V'''era '''Cr'''uz '''M'''o'''n'''trant ses '''Fe'''sses, '''Co'''mme la '''Ni'''mphe (nymphe) '''Cu'''pide '''Z'''é'''n'''a. '''Ga'''llien et '''Ge'''rard, '''As'''ssis, '''Se''' '''Br'''assaient de la '''Kr'''onenbourg. »
* « '''K'''orrigan '''Ca'''pitaine '''Sc'''andinave '''Ti'''rant sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule ('''Mn''') '''Fe'''rma le '''Co'''n de '''Ni'''cole et le '''Cu'''l de '''Z'''é'''n'''obie ('''Zn''') '''Ga'''rdant '''Gé'''néreusement l’'''As'''permatique '''Se'''mence d’un '''Br'''un '''Kr'''omatique (chromatique).»
*« '''K'''évin '''Ca'''sse '''Sc'''iemment sa '''Ti'''relire et '''V'''ient '''Cr'''ier : "'''M'''ama'''n''', fait ('''Fe''') '''Co'''mme '''Ni'''cole, '''Cu'''isine !". '''Z'''hi'''n'''g lui dit : "dé'''GaGe''', '''AsSe'''z '''Br'''aillé '''Kr'''étin".
* « '''K'''arine '''Ca'''lcula que '''Sc'''ientifiquement '''Ti'''tillé, '''V'''incent '''Cr'''ame '''M'''o'''n''' '''F'''outr'''e''' '''Co'''mme '''Ni'''colas '''Cu'''pide '''Z'''i'''n'''zin '''Ga'''lleux '''Ge'''sticulant '''As'''ymétriquement et '''Se''' '''Br'''ulant à la '''Kr'''yptonite. »
* « '''K'''ptain '''Ca'''ca '''Sc'''andinave '''Ti'''re sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule (Mn) des '''Fe'''sses de '''Co'''rine '''Ni'''çoise, '''Cu'''ltivée et '''Z'''e'''n''' (Zn), '''Ga'''lamment '''Ge'''néreuse, '''As'''sez '''Se'''xy et '''Br'''anlant '''K'''a'''r'''im (Kr). »
* « '''K'''évin '''Ca'''tapulta '''Sc'''iemment '''Ti'''bère le '''V'''erreux, le '''Cr'''étin, le '''M'''écha'''n'''t, '''Fe'''roce, '''Co'''rrompu. Ha'''Ni'''bal, '''Cu'''i'''Z'''a'''n'''t, '''Ga'''ve '''Ge'''ntillement d''''As'''pirine '''Se'''c '''Br'''utus le '''Kr'''asseux. »
* « '''K'''arine '''Ca'''ressa '''S'''e'''c''' '''Ti'''mothée '''V'''ers sa '''Cr'''oupe '''M'''ais '''n'''e '''Fé'''(fait) '''Co'''uille '''Ni''' '''Cu'''l. '''Z'''ho'''n'''g '''Ga'''gna '''Ge'''ntiment '''A s'''e '''Br'''anler '''Kr'''asseusement. »
* « '''K'''oalas de '''Ca'''nberra, '''S'''’é'''c'''ria-'''T'''-'''i'''l, je '''V'''eux '''Cr'''oire '''M'''o'''n''' '''F'''r'''è'''re '''Co'''mplètement ! Ils '''Ni'''chent, '''C'''op'''u'''lent en '''Z'''o'''n'''ages '''Ga'''lamment '''Gé'''rés, '''As'''sistés '''Se'''ulement de '''Br'''ouillons '''K(r)'''yptés. (variante : de '''Br'''aves '''K'''angou'''r'''ous)»
===== [[w:Éléments de la période 5|Période 5]] =====
''Pour : '''R'''u'''b'''idium, '''S'''t'''r'''ontium, '''Y'''ttrium, '''Z'''i'''r'''conium, '''N'''io'''b'''ium, '''Mo'''lybdène, '''T'''e'''c'''hnétium, '''Ru'''thénium, '''Rh'''odium, '''P'''alla'''d'''ium, '''A'''r'''g'''ent, '''C'''a'''d'''mium, '''In'''dium, '''S'''ta'''n'''num (Étain), '''S'''ti'''b'''ium (Antimoine), '''Te'''llure, '''I'''ode, '''Xé'''non.''
* « '''R'''o'''b'''in '''S'''u'''r''' '''Y'''vette a le '''Z'''èb'''r'''e '''N'''o'''b'''le de '''Mo'''nsieur '''T'''u'''c''' '''Ru'''. '''R'''o'''h'''an '''P'''ru'''d'''emment '''Ag'''é '''C'''é'''d'''a '''In'''évitablement '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''dées '''Xé'''nophobes. » (variante : '''R'''o'''b'''in '''S'''o'''r'''t '''Y'''von, le '''Z'''èb'''r'''e '''N'''o'''b'''le, dans '''Mo'''n '''T'''a'''c'''ot '''R'''o'''u'''illé)
* «'''R'''o'''b'''ert '''S'''enio'''r''' est un '''Y'''éti du '''Z'''aï'''r'''e ou un '''N'''o'''b'''lio de '''Mo'''dène. Le '''T'''e'''c'''hnicien '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, il '''I'''nsulte '''S'''ai'''n'''t '''S'''e'''b'''astien, un '''Te'''rrien '''I'''diot et '''Xé'''nophobe. »
* « '''R'''u'''b'''y, '''S'''o'''r'''te de '''Y'''éti '''Z'''aï'''r'''ois qu’un '''N'''o'''b'''liau de '''Mo'''dène, '''T'''e'''c'''hniquement '''Ru'''iné, '''R'''ac'''h'''ète au '''P'''ala'''d'''in avec l’'''A'''r'''g'''ent des '''C'''a'''d'''eaux '''In'''digènes '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''res '''Xé'''nophobes. »
* « Le '''R'''a'''b'''bin '''S'''o'''r'''t son '''Y'''acht, le '''Z'''èb'''r'''e, pendant que '''N'''a'''b'''il, le '''Mo'''ldave '''T'''ur'''c''' '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, '''In'''diquant à '''S'''o'''n''' ami '''S'''é'''b'''astien la '''Te'''rre '''I'''mbibée de '''Xé'''rès. »
* « '''R'''o'''b'''in '''S'''′'''r'''approche du '''Y'''éti sur un '''Z'''èb'''r'''e, '''No'''nobstant le '''Mo'''rse '''Tc'''hèque en '''Ru'''t et le '''Rh'''inocéros '''P'''é'''d'''é '''Ag'''ressif et '''C'''an'''d'''ide ; '''In'''capable de '''Sn'''iffer du '''S'''a'''b'''le et de la '''Te'''rre en '''I'''mitant '''Xé'''na. »
* «'''R'''o'''b'''ert '''S'''enio'''r''', un '''Y'''éti du '''Z'''aï'''r'''e, '''N'''o'''b'''le et '''Mo'''rose, '''T'''ri'''c'''otait '''Ru'''e du '''Rh'''um et, '''P'''en'''d'''ant l′'''Ag'''enouillement du '''C'''i'''d''', '''In'''sulta '''Sn'''obes et '''Sb'''ires '''Te'''rrorisés à l′'''I'''rruption d′un '''Xé'''nophobe. »
===== [[w:Éléments de la période 6|Période 6]] =====
''Pour : '''C'''é'''s'''ium, '''Ba'''ryum, '''La'''nthane, '''H'''a'''f'''nium, '''Ta'''ntale, '''W'''olfram (Tungstène), '''R'''h'''é'''nium, '''Os'''mium, '''Ir'''idium, '''P'''la'''t'''ine, '''Au'''rum (Or), '''H'''ydrar'''g'''irum (Mercure), '''T'''ha'''l'''lium, '''P'''lom'''b''', '''Bi'''smuth, '''Po'''lonium, '''A'''s'''t'''ate, '''R'''ado'''n'''.'' (La ou Lu selon la classification)
* « '''C'''é'''s'''ar '''Ba'''lade '''La''' '''H'''i'''f'''i de '''Ta'''ta dans le '''W'''agon et '''Re'''garde '''Os'''si '''Ir'''ma. '''P'''e'''t'''er '''Au''' '''H'''an'''g'''ar, un '''T'''e'''l''' '''P'''ro'''b'''lème '''Bi'''en '''Po'''sé '''At'''tend '''R'''épo'''n'''se. » (variante : et '''Re'''garde '''Os'''ciller '''Ir'''ma)
* « '''C'''é'''s'''ar '''Ba'''isa '''La'''ngoureusement l''''H'''orri'''f'''iante '''Ta'''ntouse dans les '''W'''C ('''Ré'''pugnants), '''Ré'''pétant les '''Os'''cillations '''Ir'''resistibles du '''P'''é'''t'''ard d''''Au'''rélien ; Mercure('''Hg''') lui '''T'''ai'''l'''la dans le '''P'''lom'''b''' une '''Bi'''te '''Po'''ilue pour lui '''A'''s'''t'''iquer les '''R'''ei'''n'''s. »
* « '''C'''a'''s'''imir et '''Ba'''stien '''La'''ncent des '''H'''yper'''f'''réquences qui '''Ta'''pent sur un '''W'''agon '''Re'''mpli d''''Os''' '''Ir'''radiés, qui '''P'''é'''t'''a à l''''Au'''be '''H'''y'''g'''iénique, '''T'''é'''l'''éportant un '''P'''lom'''b'''ier '''Bi'''zarre '''Po'''lonais '''At'''taché à la '''R'''ei'''n'''e. »
===== [[w:Éléments de la période 7|Période 7]] =====
Pour : '''Fr'''ancium, '''Ra'''dium, '''Ac'''tinium, '''R'''uther'''f'''ordium, '''D'''u'''b'''nium, '''S'''eabor'''g'''ium, '''B'''o'''h'''rium, '''H'''a'''s'''sium, '''M'''ei'''t'''nérium, '''D'''arm'''s'''tadtium, '''R'''oent'''g'''enium, '''C'''oper'''n'''icium
* « Les '''Fr'''ancais '''Ra'''lent '''Ac'''tivement depuis que '''R'''a'''f'''farin a '''D'''ou'''b'''lé '''S'''é'''g'''olène, é'''B'''a'''h'''ie par son '''H'''i'''s'''toire '''M'''ue'''t'''te sur la '''D'''i'''s'''tribution '''R'''é'''g'''ionale de la '''C'''o'''n'''nerie. »
* « '''Fr'''anck '''Ra'''te '''Ac'''tuellement le '''R'''on'''f'''lement '''D'''é'''b'''ile du '''S'''i'''g'''nal '''B'''o'''h'''émien à l''''H'''i'''s'''toire '''M'''y'''t'''hique... »
===== [[w:Lanthanides|Lanthanides]] =====
''Pour : ('''La'''nthane), '''Cé'''rium, '''Pr'''aséodyme, '''N'''éo'''d'''yme, '''P'''ro'''m'''ethium, '''S'''a'''m'''arium, '''Eu'''ropium, '''G'''a'''d'''olinium, '''T'''er'''b'''ium, '''Dy'''sprosium, '''Ho'''lmium, '''Er'''bium, '''T'''hulliu'''m''', '''Y'''tter'''b'''ium, '''Lu'''técium''
* « '''Cé'''dric, '''Pr'''ophète '''N'''éan'''d'''ertalien, '''P'''ro'''m'''et la '''S'''a'''m'''ba. '''Eu'''gène, '''G'''ran'''d''' '''T'''rou'''b'''le '''Dy'''namique des '''Ho'''mmes '''Er'''rants, '''T'''o'''m'''be sur l'h'''Yb'''ride '''Lu'''ne. »
* « La '''Ce'''llule du '''Pr'''ofesseur est à '''N'''otre-'''d'''ame de '''P'''ana'''m'''e,'''S'''o'''m'''met de l' '''Eu'''rope, '''G'''ran'''d'''e et '''T'''rès''' b'''elle, où '''Dy'''onisos et '''Ho'''mère '''Er'''raient sans '''T'''u'''m'''ulte. '''Y'''a'''b'''on '''Lu'''tèce! »
* « '''Ce'''cile, qui '''Pr'''atiquait le '''N'''u'''d'''isme, '''P'''ro'''m'''ettait à la '''S'''a'''m'''aritaine '''Eu'''phorique un '''G'''o'''d'''emiché en '''T'''u'''b'''e de '''Dy'''namite, '''Ho'''chet '''Er'''otique et '''T'''u'''m'''éfiant, s'''Y'''m'''b'''ole de '''Lu'''xure. »
* « '''Ce'''sar se '''Pr'''omène avec '''N'''a'''d'''ine et '''P'''a'''m'''ina, en '''S'''e'''m'''ant les '''Eu'''nuques qui '''G'''ar'''d'''aient le '''T'''éné'''b'''reux '''Dy'''lan dans un cac'''Ho'''t car il '''Er'''rait près de la '''T'''o'''m'''be d''''Yb'''-'''Lu'''"
* « '''Ce''' '''P'''a'''r'''adis que '''N'''ous '''d'''onna ('''Nd''') '''P'''ro'''m'''éthée, '''S'''e'''m'''blable à l''''Eu'''rope, nous '''G'''ar'''d'''e des '''T'''erri'''b'''les '''Dy'''sputes à l''''Ho'''rizon. Nous '''Er'''igerons une '''T'''o'''m'''be et '''Y''' '''b'''annirons '''Lu'''cifer »
===== [[w:Actinides|Actinides]] =====
''Pour : ('''Ac'''tinium), '''Th'''orium, '''Pr'''otactinium, '''U'''ranium, '''N'''e'''p'''tunium, '''P'''l'''u'''tonium, '''Am'''ericium, '''C'''uriu'''m''', '''B'''er'''k'''élium, '''C'''ali'''f'''ornium, '''E'''in'''s'''teinium, '''F'''er'''m'''ium, '''M'''en'''d'''élénium, '''No'''bélium, '''L'''aw'''r'''encium.''
* « '''Th'''éo '''Pa'''rle '''U'''niversellement mais ''' N' '''ex'''p'''rime '''P'''l'''u'''s l''''Am'''ertume '''C'''o'''m'''mune. '''B'''roo'''k''' '''C'''on'''f'''ie l''''Es'''poir de '''F'''or'''m'''er un '''M'''on'''d'''e '''No'''uveau et '''L'''ib'''r'''e. »
* « L''''Ac'''tivation''' Th'''ermique des '''Pa'''tates à l''''U'''ranium du '''N'''é'''p'''al en '''Pu'''rée '''Am'''ène, '''C'''o'''m'''me à '''B'''ang'''k'''ok, le '''C'''on'''f'''ort '''Es'''thétique d'une '''F'''a'''m'''ine un '''M'''i'''d'''i de '''No'''ël au '''L'''ibé'''r'''ia. (ou en bord de '''Lw'''oire selon les classifications) »
* « '''Th'''or '''Pa'''rtit '''U'''ne '''N'''uit '''p'''our '''P'''l'''u'''ton, '''Am'''oureux de '''C'''a'''m'''ille. '''B'''er'''k'''eley, '''C'''ali'''f'''ornia '''Es'''perait '''F'''u'''m'''er '''M'''a '''d'''ouce et '''No'''ble '''L'''au'''r'''a.»
===== [[w:Alcalin|Alcalin]]s Groupe 1 =====
''Pour : ('''H'''ydrogène, non alcalin), '''Li'''thium, '''Na'''trium (Sodium), '''K'''allium (Potassium), '''R'''u'''b'''idium, '''C'''é'''s'''ium, '''Fr'''ancium.''
* « ('''H'''eureux) dans le '''Li'''t de '''Na'''tacha, [[w:Khrouchtchev|'''K'''hrouchtchev]] '''R'''a'''b'''aissait '''C'''on'''s'''tamment son '''Fr'''oc. »
* « '''L'''’'''i'''nter'''Na'''tionale '''K'''ommuniste '''R'''e'''b'''ute les '''C'''apitali'''s'''tes '''Fr'''ançais. »
* « '''Li'''li '''N’a''' '''K''' '''R'''e'''b'''outonner '''C'''e'''s''' '''Fr'''ocs ('''Fr'''usques). »
===== [[w:Alcalino-terreux|Alcalino-terreux]] Groupe 2 =====
''Pour : '''Bé'''ryllium, '''M'''a'''g'''nésium, '''Ca'''lcium, '''S'''t'''r'''ontium, '''Ba'''ryum, '''Ra'''dium.''
* « '''Bé'''bel(mondo) '''M'''an'''g'''eait du '''Ca'''ssoulet '''S'''u'''r''' un '''Ba'''teau '''Ra'''pide. »
* « '''Bé'''bert '''M'''an'''g'''ea du '''Ca'''nard '''S'''u'''r''' un '''Ba'''teau-'''Ra'''dar. »
* « '''Bé'''ta '''M'''an'''g'''ea du '''Ca'''ca '''S'''u'''r''' le '''Ba'''r de '''Ra'''bat (Maroc). »
* « '''Bé'''atrice '''M'''an'''g'''ea une '''Ca'''rotte en '''S'''i'''r'''otant un '''Ba'''nana-split '''Ra'''vissant. »
===== Groupe 13 =====
''Pour '''B'''ore, '''Al'''uminium, '''Ga'''llium, '''In'''dium, '''T'''ha'''l'''lium.''
*"'''B'''oris '''Al'''lait '''Ga'''mbader '''In''' '''T'''ou'''l'''ouse"
===== [[w:Cristallogène|Cristallogène]]s Groupe 14 =====
''Pour : '''C'''arbone, '''S'''ilicium, '''Ge'''rmanium, '''S'''ta'''n'''num (Étain), '''P'''lom'''b'''.''
* « '''C'''es '''S'''imples '''Ge'''stes '''S'''eraie'''n'''t '''P'''ro'''b'''lématiques'''. »'''
* « '''C''' 'est '''Si''' '''Gê'''nant '''S'''a'''n'''s '''P'''u'''b'''is. »
===== [[w:Pnictogène|Pnictogène]]s Groupe 15 =====
''Pour : '''N'''itrogène (Azote), '''P'''hosphore, '''A'''r'''s'''enic, '''S'''ti'''b'''ium (Antimoine), '''Bi'''smuth.''
* « '''N'''e '''P'''as '''As'''tiquer '''S'''e'''b''' et sa '''Bi'''te. »
* « '''N'''e '''P'''as '''As'''tiquer '''S'''o'''b'''rement le '''Bi'''zuth. »
* « '''N'''e '''P'''as '''As'''tiquer le'''S b'''outs de '''Bi'''te. »
* « '''N'''e '''PAs''' '''S'''a'''b'''rer Byzance('''Bi'''). »
* " '''N'''e '''P'''as '''As'''soir '''S'''a'''b'''rina '''Bi'''zarrement"
===== [[w:Chalcogène|Chalcogène]]s Groupe 16 =====
''Pour : '''O'''xygène, '''S'''oufre, '''Sé'''lénium, '''Te'''llure, '''Po'''lonium.''
* « '''O'''live '''S'''uce le '''Se'''xe '''Te'''ndu de '''Po'''peye. »
* « '''O'''hh '''S'''uce moi le '''Se'''xe et les '''Te'''sticules '''Po'''ilus. »
* « '''O'''scar '''S'''uce '''Se'''s '''Te'''sticules '''Po'''ilus. »
* « '''O'''rgasme '''S'''ur le '''Se'''duisant '''Te'''odore '''Po'''ilu. »
* « '''OS''' '''Se'''dimentaire '''Te'''rriblement '''Po'''li. »
* « '''O'''h '''S'''acré '''Se'''igneur aux '''Te'''sticules '''Po'''lyèdriques. »
* « '''O'''h '''S'''eigneur '''Sé''' (c'est) '''Te'''llement '''Po'''urri. »
===== [[w:Halogène|Halogène]]s Groupe 17 =====
''Pour : '''F'''luor, '''C'''h'''l'''ore, '''Br'''ome, '''I'''ode, '''A'''s'''t'''ate.''
* « '''F'''ootball '''Cl'''ub de '''Br'''èles '''I'''ncapables d''''At'''taquer. »
* « '''F'''ranck et '''Cl'''aude '''Br'''outent '''I'''rène '''A''' '''t'''able. »
* « Une '''F'''issure '''Cl'''aviculaire '''Br'''isa tout '''I'''ntérêt d''''At'''taquer. »
* « '''F'''outez '''Cl'''aire, qui '''Br'''anle '''I'''saac, car elle '''At'''tend. »
* « '''F'''antastique, '''Cl'''aire '''Br'''anche '''I'''nstinctivement l''''At'''tache. »
* « '''F'''erdinand '''Cl'''aque '''Br'''utalement '''I'''rène '''A''' '''t'''erre. »
* « Les '''F'''ameuses '''Cl'''ochettes des '''Br'''ebis d''''I'''talie '''At'''tirent. »
* « Le '''F'''ranc '''Cl'''ovis '''Br'''oie d''''I'''nnombrables '''At'''omes. »
* « '''F'''élicie '''Cl'''aqua '''Br'''ian, '''I'''nnocent '''At'''tardé. »
* « '''F'''olle '''Cl'''ara '''Br'''ave l''''I'''nvincible '''At'''hena. »
===== [[w:Gaz noble|Gaz noble]]s Groupe 18 =====
''Pour : '''Hé'''lium, '''Né'''on, '''Ar'''gon, '''Kr'''ypton, '''Xé'''non, '''R'''ado'''n'''.''
* « '''He'''rcule '''Né'''gligea d’'''Ar'''racher le '''K'''o'''r'''sage de '''Xé'''na et '''R'''o'''n'''fla. »
* « '''Hé''','''Né'''ron, '''Ar'''rête de '''Kr'''âner, '''Xé'''nophobe '''R'''i'''n'''gard ! »
===== [[w:Métalloïde|Métalloïde]]s =====
''Pour : '''B'''ore, '''Si'''licium, '''Ge'''rmanium, Arsenic '''As''', Antimoine '''Sb''', '''Te'''llure et '''Po'''lonium''
*« '''B'''ob '''Si'''ffle son '''Ge'''t '''As'''sis avec '''S'''é'''b''' devant la '''Té'''lé '''Po'''lonaise. »
==== Couples acide/base ====
L’a'''c'''i'''d'''e '''c'''è'''d'''e un ou plusieurs protons tandis que la b'''a'''se c'''a'''pte un ou plusieurs protons.
==== Couples oxydant/réducteur ====
« Les électrons sont du côté de l'Occident. » (phonétiquement ''l’oxydant'')
On peut également retenir que :
**Ox Fixe, Red Cède (L'oxydant fixe des électrons, le réducteur en cède)
** l’oxyd'''ant''' est méch'''ant''' (il prend donc des électrons) ;
** le réduct'''eur''' a bon c'''œur''' (il donne donc des électrons).
**L'oxydANT gagnANT, réductEUR donnEUR
**Cap sur l'occident ! (L'oxydant '''cap'''te les électrons)
**Notons aussi que l'oXydant aXepte (accepte) les électrons.
Phrase qui marche à la fois pour les couples Acide/Base et Oxydant/Réducteur : L'Apéro gagne toujours ! ( A[cide] perd (des protons), O[xydant] gagne (des électrons) ).
'''<nowiki/>'''
==== Ordre de priorité des groupements radicaux dans la nomenclature====
'''<nowiki/>'''
'''A'''bruti '''H'''ans '''est''' l' '''ami de''' '''Nitr'''o! '''Al'''lez '''c'''hantons, L''''alcool''' '''am'''ené '''i'''ci '''e'''st '''t'''rès '''t'''errible
Acide carboxylique, Halogénure, Ester, Amide, Nitrile, Aldéhyde, Cétone, Alcool, Amine, Imine, Ether, Thiol, Thioléter (pour ces deux derniers se reporter à la longueur).
'''A Carbalo''' '''Ester''' '''a mit''' du '''Nitrile Aldéhyde''', '''s'étonne''' '''Alcolaminimine'''. '''Et tertio''', du '''thioleter'''.
"L'''oïc''' '''est''' '''l’ami de Dalton":''' acide carboxylique (-oïque), ester, amide, aldéhyde, cétone.
(ne pas confondre l'aldéhyde et l'alcool- voir la longueur des mots: c'est le plus long qui gagne).
'''<u>Règle de Cahn, Ingold et Prelog</u>'''
<u>''pour '''I '''> '''Br''' > '''Cl''' > '''S''' > '''F '''> '''O''' > '''N''' > '''C''' > '''H'''''</u>
** « '''Ib'''ra '''Cl'''ame '''S'''a '''F'''oi '''O''' '''N'''ouveau '''C'''avani '''H'''éroïque. »
=== Thermodynamique ===
==== Loi des gaz parfaits ====
'''pV''' = '''nRT'''
'''p''' = pression en pascals ; '''V''' = volume en mètres cubes ; '''n''' = quantité de matière en mols ; '''R''' = constante des gaz parfaits. R = 8,3 J.K-1.mol-1 ; '''T''' = température en Kelvins
'''P'''ascal '''v'''oulut '''n'''ous '''r'''endre '''t'''héiste (référence au pari de Pascal)
'''P'''a'''v'''a'''n'''e'''r'''ai'''t''' (sans les voyelles)
'''pV''' = '''nRT''' n’est pas pété, énervé (ptnrv)
'''P'''rocès-'''v'''erbal ; '''n'''ous '''r'''end '''t'''riste
'''P'''uissance de '''V'''itesse = '''n'''otion de '''R'''apidité '''T'''errestre - Pour les joueurs de jeu de rôle uniquement !
Les PV d'un Pokemon est égale au Niveau fois sa RésisTance
<br>
==== Différentielle de l’enthalpie ====
dH = TdS + VdP
'''d'''îners '''H'''onorables = '''T'''artes '''d'''e '''S'''aison + '''V'''ins '''d'''u '''P'''ays (dH=TdS + VdP)
'''d'''ouces '''H'''armonies = '''T'''oniques '''d'''e '''S'''olfège + '''V'''ibrations '''d'''e '''P'''iano (dH=TdS + VdP)
'''d'''écouvertes '''H'''éroïques = '''T'''résors '''d'''e '''S'''able + '''V'''oyages '''d'''e '''P'''irates (dH=TdS + VdP)
'''d'''anses '''H'''ispaniques = '''T'''angos '''d'''e '''S'''eville + '''V'''alses '''d'''e '''P'''ampelune (dH=TdS + VdP)
'''d'''épart '''H'''éroïque = '''T'''oujours '''d'''u '''S'''tyle + '''V'''itesse '''d'''e '''P'''ointe (dH=TdS + VdP)
'''d'''estination des '''H'''istoriens = '''T'''raversant '''d'''es '''S'''iècles + '''V'''oyageant '''d'''ans le '''P'''assé (dH=TdS + VdP)
==== Différentielle de l’enthalpie libre ====
dG = VdP-SdT
'''V'''iande '''d'''e '''P'''orc '''-''' '''S'''el '''d'''e '''T'''able (VdP-SdT)
'''V'''ends '''d'''u '''P'''ain sans (-) '''S'''ortir '''d'''e '''T'''on '''G'''îte (VdP - SdT = dG)
==== Différentielle de l’énergie interne (sans variation de quantité de matière) ====
dU=TdS-PdV
'''T'''u '''d'''ois '''S'''avoir mais '''P'''as '''d'''e'''V'''iner (TdS-PdV)
'''T'''out '''d'''e '''S'''uite '''Moins''' de '''P'''oints '''d'''e '''V'''ie
'''T'''éter '''D'''u '''S'''el '''P'''endant '''D'''eux (ou '''D'''ix) '''V'''endredis.
'''T'''rou '''d'''ans le '''S'''lip et '''P'''antalon '''d'''ans le '''V'''ent
'''<nowiki>d'</nowiki>'''après '''U'''lysse = '''T'''outes '''d'''es '''S'''irènes mais '''P'''as '''d'''es '''V'''ampires
<br />
==== Différentielle de l’énergie interne (avec variation de quantité de matière) ====
dU=TdS-PdV+µdn
'''T'''rop '''d'''e '''S'''avoir mais '''P'''as '''d'''<nowiki/>'en'''V'''ie c'est être '''nu''' '''d'''ans la '''n'''uit
=== Géologie ===
'''Les ères géologiques, du Quaternaire au Primaire (permettant de retenir les datations approximatives 7x60 + 3x40 Ma)'''
'''Cénozoïque''' 60 (Quaternaire + Tertiaire) '''Crétacé''' 120 '''Jurassique''' 180 '''Trias''' 240 '''Permien''' 300 '''Carbonifère''' 360 '''Dévonien''' 420 '''Silurien''' 460 '''Ordovicien''' 500 '''Cambrien''' 540
'''C'''ite '''C'''e '''J'''oli '''T'''ruc '''P'''our '''C'''onnaître '''D'''es '''S'''iècles '''O'''rdonnés '''C'''orrectement
==== Niveaux de l'échelle chronologique géologique ====
'''Éo'''le '''ér'''adiqua les '''pe'''upliers '''ép'''uisés par l''''âge'''.
* (éon, ère, période, époque, âge)
==== Les six périodes géologiques de l’ère primaire ====
;Cambrien, Ordovicien, Silurien, Dévonien, Carbonifère, Permien.
* '''''Cambr'''onne, l’'''ord'''urier,''' s’il eû'''t été '''dévo'''t, n’eût point '''carboni'''sé son '''pèr'''e''
* '''''Cambr'''onne '''ordo'''nna '''sil'''ence et '''dévo'''uement à ses '''car'''abiniers '''perm'''issionnaires''
* '''''Cambr'''onne '''aur'''ait, '''s'il eût''' été '''dévo'''t, '''carboni'''sé son '''pèr'''e''
* '''c-or-si-dé-ca-pé''' = Corps si décapés.
* ''Le '''ca-or-sil-dé-ca-pe''' ='' Le Cahors, il décape.
==== Les trois périodes géologiques de l’ère secondaire ====
;Trias, Jurassique, Crétacé.
* '''T'''rois '''j'''ours '''c'''hacune.
==== Les cinq périodes géologiques de l’ère tertiaire ====
;Paléocène, Éocène, Oligocène, Miocène, Pliocène.
* '''Pâl'''e '''Et o'''bscène '''Au lit''', '''Mio''' se '''Plie au'''x scènes (de l'amour)
==== Stalactites et stalagmites ====
''Les stalac'''t'''ites '''t'''ombent, les stalag'''m'''ites '''m'''ontent.''
==== Géophysique ====
Formule pour la [http://fr.wikipedia.org/wiki/Anomalie_de_Bouguer correction gravitationnelle de Bouguer]:
2*π*h*ρ*G
(G=constante gravitationnelle ρ=Masse volumique/Densité)
"deux pies hachent Roger"
2 π h ρ G
(ρ = "Rho", lettre grecque)
==== [[w:Échelle de Mohs|Échelle de Mohs]] ====
"'''T'''a '''G'''rosse '''C'''oncierge '''F'''olle d''''A'''mour '''O'''se '''Q'''uémander '''T'''es '''C'''aresses '''D'''ivines"
"'''T'''oi '''G'''rand '''C'''hevalier, '''F'''uis '''A'''vec '''O'''rdre '''Q'''uand '''T'''on '''C'''œur '''D'''éfaille"
"'''T'''rès '''G'''rand '''C'''hemin de '''F'''er '''A'''pache. '''O'''h ! '''Q'''uel '''T'''emps '''C'''e '''D'''imanche !"
"'''T'''on '''G'''igolo '''C'''onte '''F'''leurette '''A''' '''(H)O'''rtense, '''Q'''ui '''T'''e '''C'''ocufie '''D'''iablement !"
"'''T'''on '''G'''rand '''C'''ul '''F'''endu '''A''' une '''O'''uverture '''Q'''ue '''T'''u '''C'''aches '''D'''écemment"
"'''T'''on '''G'''ros '''C'''ochon '''F'''ait '''A'''ïe '''O'''uille '''Q'''uand '''T'''u '''C'''ognes '''D'''essus"-[[Émilien]]
([[w:Talc|'''T'''alc]], [[w:Gypse|'''G'''ypse]], [[w:Calcite|'''C'''alcite]], [[w:Fluorite|'''F'''luorite]], [[w:Apatite|'''A'''patite]], [[w:Orthose|'''O'''rthose]], [[w:Quartz (minéral)|'''Q'''uartz]], [[w:Topaze|'''T'''opaze]], [[w:Corindon|'''C'''orindon]], [[w:Diamant|'''D'''iamant]])
=== Botanique ===
==== Distinguer les hêtres des charmes ====
Le '''charme''' d''''Adam''' est d''''être''' à '''poil'''.
ou encore: "Être à poils charme Adam"
La feuille du charme a des dents (charme d’Adam)
et la feuille du hêtre a des poils (être à poil).
==== Distinguer les principales espèces de pin ====
Les aiguilles du pin '''blanc''' sont groupées par '''5'''.
'''Blanc''' a '''5''' lettres.
Le pin '''rouge''' a des aiguilles groupées par '''2'''.
Le mot '''rouge''' a '''2''' syllabes (s'il est suivit d'un mot commençant par une consonne en versification).
Les aiguilles du pin '''noir''' sont en groupes de '''2'''.
Dans le mot '''noir''', il y a '''2''' voyelles.
==== Distinguer les sapins des épicéas ====
Les sapins ('''''A'''bies'') ont des cônes '''a'''scendants, les épicéas ('''''P'''icea'') ont des cônes '''p'''endants.
==== Distinguer les cèdres ====
Le cèdre de l’'''A'''tlas a les pointes des branches '''a'''scendantes, le cèdre de l’Himalaya (''Cedrus '''d'''eodara'') '''d'''escendantes, le cèdre du '''L'''iban horizonta'''l'''es.
==== Distinguer les platanes des érables ====
pl'''A'''t'''A'''ne : feuilles '''A'''lternes
'''É'''rable : feuilles oppos'''É'''es
==== Distinguer un Catalpa d’un Paulownia ====
P'''a'''ulowni'''a''' : deux feuilles par nœud (2 fois le a dans le nom)
C'''a'''t'''a'''lp'''a''' : trois feuilles par nœud (3 a dans son nom)
==== Distinguer les feuilles de trèfle ====
Les feuilles du trèfle blanc ont de petites dents autour, celles du trèfle rouge ont des poils autour. Dents blanches, poils rouges (et non dents rouges, poils blancs !)
==== Distinguer les feuilles de trèfles de celles des luzernes ====
Les luzernes (''Medicago'') ont des pointes (= aiguilles, les médecins font des injections) au bout des folioles.
==== Distinguer les vesces des gesses ====
gesses ('''''L'''athyrus'') : l'alignement des points d'insertion des filets des étamines forme un angle droit avec le tube des étamines → L
vesces ('''''V'''icia'') : il est oblique par rapport au tube ; on retrouve ce côté oblique dans la lettre V
==== Distinguer les knauties des scabieuses ====
'''k'''nautie : 4 ('''k'''atr’) pétales dans chaque fleur de l’inflorescence
'''s'''cabieuse : 5 ('''s'''inq) pétales par fleur de l’inflorescence et des '''s'''oies sur le réceptacle
==== Distinguer les plantules de céréales dans un champ ====
BOAS :
le '''b'''lé étant plus riche a des oreillettes, des poils et une ligule
l’'''o'''rge a des oreillettes et une ligule
l’'''a'''voine a une ligule
le '''s'''eigle étant plus pauvre, n’a plus rien
=== Zoologie ===
==== Ordre des cétacés ====
« '''C'est assez''', dit la '''baleine''', al'''ors que''' j'ai le '''dos fin''' je me '''cache à l'eau'''
**baleine, orque, dauphin, cachalot, mais la liste est très incomplète.
==== Ordre des tatous ====
Les tatous font partie de l’ordre des ''Édentés'' car : "T’as tout sauf les dents !"
=== Biologie ===
==== L'ordre hiérarchique de la classification de [[w:Taxinomie|taxinomie]] ====
Des Rats Essayent de Courir là Où Finissent les Grands Espaces (Raccourcis).<br />
DoRs EnCOre, la Famille GÈRe.<br />
Reste En Classe Ou Fais Grandes Études.<br />
Reste En Contact, Odile, Fais Gaffe, Émile ! (inspiré de la Cité de la peur, où Odile est attachée de presse et Émile tueur)<br />
Domaine, Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce, (Race).
RECOFGE: Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce
==== Les [[w:bases azotées|bases azotées]] de l’ADN ====
'''À''' '''T'''on '''G'''rand '''C'''œur.
'''A'''h '''T'''a '''G'''ueule '''C'''rétin
'''À''' '''T'''able '''G'''rand '''C'''hef
('''ATGC''' : [[w:adénine|adénine]], [[w:thymine|thymine]], [[w:guanine|guanine]], [[w:cytosine|cytosine]])
==== Intron/Exon ====
'''Int'''ron = '''Int'''rus ou '''int'''rusif, c'est la partie de nucléotide d'un gène qui est excisé de l'ARN lors de l'épissage, à l'inverse des '''exons.'''
'''<nowiki/>'''
==== La séquence nucléotidique des télomères humains ====
'''T'''ous '''t'''es '''a'''mis se '''g'''avent de '''g'''énial '''g'''uarana.
(TTAGGG)
==== Les différentes phases de la [[mitose]] ====
**le Prophète Athée (Pro Met A T)
** Je te '''ProMets''' de l''''Ana'''l au '''Telo'''
** '''ProMets''' à '''Anna''' de '''Tél'''éphoner
** '''P'''etit '''M'''ammifère '''À''' '''T'''éton
** '''P'''etit '''M'''atin '''A'''uprès de '''T'''oi
** '''P'''etit '''M'''artien '''A'''ttaque la '''T'''erre
** '''P'''etite '''M'''émé '''A''' '''T'''éléphoné
** '''P'''apa '''M'''aman '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''mi '''Th'''omas
** '''P'''our '''M'''on '''A'''mour '''T'''oujours
** '''P'''our '''M'''on '''A'''nus '''T'''roué
** '''P'''etite '''M'''ademoiselle '''A'''ge '''T'''endre
** Promettante : '''Pro'''/'''met'''t/'''an'''/'''te'''
** '''PROMETANATELO'''
** ou ProMéthée est AnaTello (Se rappeler de la phrase Prométhée est un intello)
** '''P'''apa '''M'''ange '''A''' '''T'''able
** '''P'''ro des '''M''' '''A''' '''T'''hs
** '''P'''rof de '''M''' '''A''' '''T'''hs
** '''P'''ierre '''M'''angea des '''A'''nanas '''T'''ransgéniques
** '''P'''ouvoir '''M'''asculin '''A'''vant '''T'''out
** '''P'''aris-'''M'''arseille '''A''' '''T'''rotinette
** '''P'''our '''M'''émoriser : '''A'''voir '''T'''ravaillé
** '''P'''our '''M'''ieux '''A'''pprendre'''T'''out
** '''P'''ays les '''M'''oins '''avancés'''
** Le '''Pro'''f '''Met''' l' '''Âne''' devant la '''Tél'''é
('''P'''rophase, '''M'''étaphase, '''A'''naphase, '''T'''élophase)
** '''P'''auline '''M'''arche '''à''' la '''T'''équila
** TAMPI (à lire à l'envers)
** c'est PRoMEtteur An(un) inTello
** Papa Mange un Abricot Trop sucré (avec le s de sucré pour la synthèse qui suit la mitose G1 --> S --> G2)
==== Les différentes phases de la PROPHASE ====
'''Le''' '''Zi'''zi du '''Pachy'''derme a des '''Di'''mensions '''Dia'''boliques.
(Leptotène, Zygotène, Pachytène, Diplotène, et Diacinèse)
ou
Letzplin (lepto/zygo) protége (pachy) didier (diplo/diacinèse)
ou
Le Zip à Didier
ou
Le zizi n'a pas de diarrhée
ou
Le zizi poilu du doyen
ou
Le zizi du pachyderme et du diplodocus sont différents
ou
Les Zizis Peuvent Devenir Durs !
ou '''Pré'''férer '''Le''' '''Zi'''zi du '''Pachy'''derme à celui du '''Diplo'''docus '''Dia'''bétique
Pour préleptotène, leptotène, zygotène, pachytène, diplotène, diacinèse
==== Les acides aminés dits essentiels : ====
''on compte neuf acides aminés essentiels : le tryptophane, la lysine, la méthionine, la phénylalanine, la thréonine, la valine, la leucine, l'isoleucine et l'histidine''
Le (LEU) trou (THR) de l'hystérique (HIS) Lyse (LYS) fait (PHE) tripper (TRY) valentin (VAL) mais (MET) ilose (ILE) pas !
'''''Le''' '''très''' '''ly'''rique '''Tri'''stan '''fait''' '''va'''chement '''m'''archer '''Ys'''eult, quelle '''Hist'''oire !''
ou encore :
Hystérique, le très lyrique Tristan fait vachement méditer Iseult en Argentine (His)Leu-Thr-Lys-Trp-Phe-Val-Met-Iso(Arg): Histidine et Arginine seulement essentiels chez les enfants.
('''Le'''ucine, '''Thré'''onine, '''Ly'''sine, '''Try'''ptophane, '''Phé'''nylalanine, '''Va'''line, '''M'''éthionine, '''Is'''oleucine, '''Hist'''idine)
Met le dans la valise, il fait trop d'histoire avec l'argent/en argentine.
le cours d’'''hist'''oire, '''il''' '''le''' '''lit''' '''mais''' '''fait''' '''tres''' '''trivial'''
('''Hist'''idine; '''Ile''': Isoleucine, '''Leu'''cine, '''Ly'''sine, '''Mé'''thionine, '''Phé'''nylalanine '''Thré'''onine,'''Try'''ptophane,, '''Va'''line)
Dans une '''V'''(aline)'''I'''(soleucine)'''L'''(eucine), il y a des '''H'''(ystidine)'''L'''(ysine)'''M'''(ethionine) et des '''P'''(hénilalanine)'''T'''(hréonine)'''T'''(ryptophane) (Dans une ville, il y a des HLM et des PTT)
''ils le valent trop trop mes félicitations''
'''ile''' '''leu''' '''val''' '''thr'''''op'' '''tr'''''o'''''p''' '''met''' '''phe''' '''lys''' ''itations''
''Va te le mettre, Phillipe''
'''VA'''l '''TH'''r '''LE'''u '''MET''' '''TR'''p '''PH'''e '''ILE''' '''LY'''s pe
'''''Va''' '''tri'''poter '''Lys'''e mais ('''met''') fait ('''phe''') '''le''' '''tr'''ès '''iso'''lément''
''val thr lys met phe (fait) leu trp ile (iso-leucine)''
Plus simple et plus concret que tous les autres moyens mnémotechniques:
VTT MILLPH (prononcé VTT MILF) et ainsi vous obtiendrez : Valine, Thréonine, Tryptophane, Méthionine, Isoleucine, Leucine, Lysine, Phénylalanine, Histidine.
==== Les acides aminés dits apolaire (Proline polaire/apolaire comprise) ====
Valérie promet à la triste Iseult le phénix et la Glycine.
(val) (Pro/Met)(Ala)(Trp) (Ile) (Leu) (Phe) (Gly)
Glycine dévale à la pelle, il le promet trop.
==== Le [[w:cycle de Krebs|cycle de Krebs]] ====
**''Si le '''citr'''on '''iso'''<nowiki>le l'</nowiki>'''acéto'''ne, le '''succi'''nct '''succès''' '''fumera''' '''m'''oins '''haut'''''
('''citr'''ate, '''iso'''citrate, alph'''acét'''oglutarate, '''succ'''inyl CoA, '''succ'''inate, '''fumara'''te, '''ma'''late, '''o'''xaloacétate)
** Avec les initiales : "C'est ici ce samedi soir : fumette, mal-à-la-tête, oubliette."
ou encore :
** ''La '''C''' '''I''' '''A''' '''su'''specte un '''su'''spect qui '''fum'''e des '''Mal'''boros '''ox'''ydées.''
** '''O'''h '''C'''atastrophe ! '''I'''l '''Os'''e '''Ac'''tiver '''Sa''' '''Su'''per '''F'''orce '''M'''agique
==== Le [[w:Cycle de Calvin|Cycle de Calvin]] ====
**"Les '''ri'''mes '''intermédiaires''' aux '''fo'''rmes '''diffo'''rmes de '''PGAL''' '''ri'''ment."
('''ri'''bulose phosphate, '''intermédiaire''' instable qui se scinde en deux 3-'''pho'''sphoglycérate, 1,3-'''dipho'''sphoglycérate, phosphoglycéraldéhyde ('''PGAL'''), dont l'un quitte le cycle et cinq sont utilisés pour reformer le '''ri'''bulose phosphate.)
==== Les Aldohexoses ====
'''Allo'''ns, '''altr'''uiste '''gl'''acer la '''mann'''e, '''Gul'''liver '''i'''ra '''gal'''érer au '''tall'''us
('''Allo'''se; '''altr'''ose; '''gl'''ucose; '''mann'''ose, '''Gu'''lose '''i'''dose '''ga'''lactose '''ta'''lose)
==== Les protéines intervenant dans les jonctions cellulaires ====
(Attention que ces phrases ne fonctionnent pleinement que si l'on connaît <i>déjà</i> les protéines intervenant, mais que l'on a du mal à retenir lesquelles font quoi.)
- Jonction Adherens : '''Vin'''t le '''cad'''avre '''é'''quipé d''''a'''rmes '''α''' qui '''plaqu'''a le '''glo'''ussant '''ca'''valier.
-> '''Vin'''culine, '''cad'''hérine-'''E''', '''a'''ctine, '''α'''-actinine, '''plak'''o'''glo'''bine, '''ca'''ténine.
- Jonction de contact focal : '''Vin'''t la '''paix''' '''intégr'''ale; les '''a'''rmes '''α''' en '''ta'''s.
-> '''Vin'''culine, '''pax'''iline, '''intégr'''ines, '''a'''ctine, '''α'''-actinine, '''ta'''line.
- Desmosome : Tout ce qui '''colle''', plus la '''kératine'''.
-> Desmo'''coll'''ine, desmo'''glé'''ine, desmo'''plak'''ine, '''plak'''oglobine, '''plak'''ophiline, '''kératine'''.
- Jonction Gap (de communication) : Elle induit une '''connexion'''.
-> '''Connex'''ines.
- Hémidesmosomes : La '''p'''yramide de '''Khé'''ops '''d'''étruit '''intégr'''alement '''la mi'''en'''ne'''.
-> '''P'''lectine, '''ké'''ratine 5 et 14, '''d'''ystonine, '''intégr'''ine α6β4, '''laminine''' 332.
- Jonction tight, ou étanche, qui comporte des "'''kissing''' points" et dont le complexe crée une "'''zonula occludens'''" : '''Embrasser''' '''Claudine''' crée une '''occlu'''sion '''acti'''ve.
-> Claudine, occludine, actine, ZO-1.
==== Les protéines des filaments intermédiaires ====
Elles diffèrent en fonction du tissu où elles se trouvent. Attention que les moyens proposés ici servent plus à retrouver la fonction d'une protéine déjà connue qu'à retenir le nom en lui-même.
- Épithéliums : Kératines.
Facile, il suffit de réfléchir un instant pour s'apercevoir que l'épithélium est bourré de kératines (couche cornée, desmosomes, ...).
- Tissu '''C'''onjonctif : '''V'''imentines.
On retient "'''CV'''".
- Tissu '''M'''usculaire : '''D'''esmines.
On retient "'''MD'''", une abréviation fréquente en anglais pour qualifier un Docteur en Médecine (Medicinæ doctor).
- Tissu Nerveux proprement dit : Protéines des neurofilaments.
Elles n'ont donc pas de nom propre, leur nom est leur fonction : des '''protéines''' dans les '''filaments''' intermédiaires des '''neuro'''nes.
- Tissu Nerveux "de soutien" (tissu glial, donc) : Protéines fibrillaires acides gliales.
Elles n'ont pas de nom propre, le nom est la fonction : Des '''protéines''' qui génèrent des '''filaments''' ('''fibrillaires''', donc) appartenant au tissu '''glial'''. La seule chose à retenir est qu'elles sont acides.
- Noyaux : Lamines.
On peut retenir qu'elles forment la '''lamina''' nucléaire, ou encore que pour arriver au noyau d'une cellule il faut la "maltraiter", et pourquoi pas la '''laminer'''.
==Technologie==
===Électronique===
====Code couleur des résistances====
Code couleur à retenir : Noir, Marron, Rouge, Orange, Jaune, Vert, Bleu, Violet, Gris, Blanc
* Ne Mangez Rien Ou Je Vous Battrai Violemment Gros Béta.
* Ne Mangez Rien Ou Je Vous Brûle Votre Grosse Barbe.
* Ne Mangez Rien Ou Jeunez Voilà Bien Votre Grande Bêtise.
=== Informatique ===
==== RJ45 croisé ====
Broches 361782'''45'''.
==== Modèle OSI ====
Le modèle OSI divise les fonctionnalités nécessaires à la communication en sept couches :
*# '''P'''hysique,
*# '''L'''iaison,
*# '''R'''éseau,
*# '''T'''ransport,
*# '''S'''ession,
*# '''P'''résentation,
*# '''A'''pplication.
** Il faut être deux pour avoir une liaison.
** Le 4*4 est un transport.
"Félicie, OSI"
*#Séduit par son '''PHYSIQUE'''
*#et n'ayant aucune '''LIAISON''',
*#je l'ai contactée sur un '''RÉSEAU''' social.
*#Arrivé chez elle en '''TRANSPORT''' en commun,
*#suivi une '''SESSION''' de va-et-vient,
*#sans aucune forme de '''PRÉSENTATION''',
*#j'y ai mis toute mon '''APPLICATION'''.
Le lendemain, elle me recontactait...
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre ci-dessus ( P L R T S P A ) :
*#'''P'''artout '''L'''e '''R'''oi '''T'''rouve '''S'''a '''P'''lace '''A'''ssise
*# '''P'''our '''L'''e '''R'''éseau '''T'''out '''S'''e '''P'''asse '''A'''utomatiquement
*# '''P'''ar '''L'''à, '''R'''aisonnons '''T'''ransport '''S'''ans '''P'''résenter l''''A'''pplication
*# ''Pour les amateurs du jeu d'échecs :'' '''P'''rends '''L'''a '''R'''eine '''T'''out '''S'''era '''P'''lus '''A'''gréable
*# '''P'''etit '''L'''apin '''R'''ose '''T'''rouvé à la '''S.P.A.'''
*# '''P'''our '''L'''e '''R'''epas '''T'''out '''S'''era '''P'''rêt '''À''' 7 heures (7 couches)
*# '''P'''our '''L'''e '''R'''éseau '''T'''u '''S'''eras '''P'''as '''A'''ugmenté
*# '''P'''our '''L'''a '''R'''oute, '''T'''u '''S'''uis '''P'''ierre-'''A'''lain !
*# '''P'''ourquoi '''L'''e''' R'''oux '''T'''ouche '''S'''on '''P'''énis '''A'''llongé ?
*# '''P'''our '''L'''a''' R'''etenir '''T'''oujours '''S'''e '''P'''arler '''A'''vant !
*# '''P'''ierre '''L'''ouis ''' R'''este '''T'''oujours '''S'''ans '''P'''énétration '''A'''nale !
*# '''P'''endant '''L'''es '''R'''ègles '''T'''oujours '''S'''évir '''P'''ar l''''A'''nus !
Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre inverse ( A P S T R L P ) :
*# '''A'''près '''P'''lusieurs '''S'''emaines, '''T'''out '''R'''espire '''L'''a '''P'''aix
*# '''A'''près '''P'''lusieurs '''S'''odomies, '''T'''out '''R'''ectum '''L'''âche '''u'''n '''P'''et
*# '''A'''vec '''P'''atrick '''S'''abatier, '''T'''u '''R'''amasses '''L'''e '''P'''ognon
*# L''''A'''méricain '''P'''uritain '''S'''e '''T'''itille ('''R'''arement|'''R'''égulièrement) '''L'''e '''P'''hallus
*# '''A'''pparemment '''P'''atrick '''S'''ebastien '''T'''on '''R'''ectum '''L'''aisse '''P'''erplexe
*# '''A'''h '''P'''etite '''S'''alope, '''T'''u '''R'''ecraches '''L'''a '''P'''urée
==== Classe d'adresse IP ====
En binaire, compter le nombre de 1 avant le premier 0.
* A : 0 -> 127 (+ 127) <code>0xxxxxxx</code>
* B : 128 -> 191 (+ 63) <code>10xxxxxx</code>
* C : 192 -> 223 (+ 31) <code>110xxxxx</code>
== Grammaire et orthographe ==
===<u>Les principaux mots interrogatifs</u>===
Ce moyen mnémotechnique est très utile pour les coups de téléphone où l’on doit demander des renseignements. Il faut dresser rapidement la liste des mots interrogatifs sur un papier et être sûr que l’on a des réponses à toutes les questions.
'''C’est cucu, c’est occupé !'''
*'''C'''ombien ?
*'''Q'''uoi ?
*'''Q'''ui ?
*'''C'''omment ?
*'''O'''ù ?
*'''Q'''uand ?
*'''P'''ourquoi ?
=== [[w:Conjonction de coordination|Conjonctions de coordination]] ===
''Mais où est donc Ornicar ?''
(Mais, Ou, Et, Donc, Or, Ni, Car)
Mais cette méthode est pédagogiquement discutable, car elle entretient la confusion entre ''et'' (conjonction) et ''est'' (verbe ''être'' à la troisième personne du singulier), ainsi qu'entre ''ou'' (conjonction) et ''où'' (adverbe ou pronom relatif).
Attention, ''donc'' n’est plus une conjonction de coordination, mais bien un verbe conjugué pour ''est'' et un adverbe de coordination pour ''où'' !
On peut aussi l’apprendre de cette manière afin de sortir le OU et ne pas induire de confusion dans l’esprit
Mais ! Et donc Ornicar (mais, et, donc, or, ni, car) en jouant sur la sonorité de la surprise
Au Québec, on dit aussi: ''Mais où est donc Carnior ?''
===<u>Les principales prépositions</u>===
*''Adam part pour Anvers avec cent sous sûrs, entre derrière chez Decontre''
:(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, Sur, Entre, Derrière, Chez, De, Contre)
*''Adam part pour envers avec deux cents sous chez Parmisur. ''
:(À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Parmi, Sur)
*''Adam part pour Anvers avec deux cents sous.''
*"Adam part pour Anvers avec deux cents sous chez Sur."
:( À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Sur)
*''Adam part pour Anvers avec cent sous de chez surdurand.''
*"(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, De, Chez, Sur, Durant)
*''Adam Surché part pour Anvers avec deux-cents sous''
*"(À, Dans, Sur, Chez, Par, Pour, En, Vers, Avec, Sans, Sous)
*’’ Adeudans part pour Sur sans sous chez Devant-derrière avec Avant-après-contre’’
===<u>Les pronoms relatifs</u>===
3 culs domptent ouvertement monsieur lequel,Duquel,Auquel...
''<nowiki>Qui que quoi dont où lequel duquel auquel…'</nowiki>''
===<u>Les déterminants possessifs</u>===
Au pluriel: ''Mais c'était nos voleurs !''
(mes, ses, tes, nos, vos, leurs)
===<u>Orthographe</u>===
* Mou'''r'''ir ne prend qu’un “ r ” car on ne meurt qu’une fois.
* Nou'''rr'''ir prend deux “ r ” car on se nourrit plusieurs fois.
* Cou'''r'''ir ne prend qu’un “ r ”car on manque d’air en courant,<br /> mais quand on a'''rr'''ive on prend tout l’air qu’on peut.
* L’hironde'''ll'''e prend deux “ l ” car elle vole avec ses deux ailes.
* La v'''i'''e'''i'''lle ne peut marcher qu’avec ses deux bâtons.
* A'''pp'''uyer prend deux « p » car on s’appuie mieux sur deux pattes.
* Un ba'''l'''ai prend un seul “ l ” car il n’y a qu’un manche.
* Un ba'''ll'''et prend deux “ l ” car pour danser il faut deux jambes.
* Toujour'''s''', toujours un “ s ” et à jamai'''s''', ne jamais l’oublier.
* J’a'''p'''erçois sur une jambe mais j’a'''pp'''arais sur les deux.
* Quand je mets deux "p" à apercevoir, j'aperçois une faute.
* Je n’a'''p'''erçois qu’un '''p''' à a'''p'''ercevoir (ou je m’a'''p'''erçois qu’a'''p'''ercevoir ne prend qu’un '''p''').
* Enve'''l'''oppe ne prend qu’un “ l ” car on ne met qu’une lettre dans une enveloppe. En revanche pour un vélo on a deux '''p'''neus : dé''velo'''''pp'''er, en''velo'''''pp'''er, etc.
* Cuiss'''eau''' de v'''eau'''.
* Sate'''ll'''ite prend 2 '''L''' car c’est plus pratique pour voler. (et un seul '''t''' car il ne tourne qu’autour d’une seule '''T'''erre)
* Évide'''mm'''ent prend deux '''m''' comme dans '''Papa/Maman''' (à noter : tous les adjectifs qui se terminent par "ent", comme "évident", prennent 2 "m" ensuite, comme "évidemment").
* Je me souviens d’une corde en rappel : On se souvient '''DE''' quelque chose, mais on se rappelle quelque chose.
* Un pa'''r'''esseux cou'''r'''onné ca'''r'''essait une ca'''r'''otte avec un air intéressé : liste de mots qui ne prennent qu’un '''r'''.
* Co'''ll'''ine a deux colonnes (2 ” l ”) et colo'''nn'''e a deux collines (2 ” n ”).
* Dé'''velopp'''er je fais du vélo avec mes deux pieds pour pédaler.
* Échapper prend deux "P" car on s'échappe mieux avec deux pieds.
* Culo'''tt'''e prend deux '''T''' car il y a deux jambes pour une culotte
* Un professeur a un seul '''F'''ront et deux '''S'''ourcils, donc un seul F, mais deux S
* Philippe : je marche (2p) mais ne vole pas (1l)
* On parle le flaman'''D''' dans les Flan'''D'''res. Le flaman'''T''' rose est un oiseau de grande '''T'''aille.
* L’am'''a'''nde pousse sur un '''a'''rbre ; l’am'''e'''nde sur un '''e'''ssuie-glace.
*Guè'''r'''e signifie "pas beaucoup", donc un seul r. Il faut au moins deux adversaires pour faire la gue'''rr'''e, donc 2 R.
* Une P'''ê'''che (melba...) / P'''ê'''cher (du poisson) / P'''é'''cher (commettre une offense) / un P'''é'''ché (originel...) : Dans la p'''ê'''che en rivière, le '''^''' représente l’hameçon et la p'''ê'''che (fruit) représente le flotteur de la canne à p'''ê'''che). Quand on confesse au prêtre un p'''é'''ché, on fait profil bas (accent aigu sur le '''é''').
* Tous les membres de la famille ont un accent grave, sauf pépé et mémé : père, mère, nièce…
* M devant Mbappé (M devant M, B et P)
* Reg versus Erg :
** Un Reg est un désert de Roches, de pieRRes
** L’ERg est un désERt de dunes
===<u>Mots avec accent circonflexe</u>===
* Une t'''a'''che c'est suffisamment sale pour ne pas avoir besoin d'en rajouter (d'accent circonflexe)
* Le chapeau de c'''i'''me est tombé dans l’ab'''î'''me. Et celui du bo'''i'''teux dans la bo'''î'''te !
* On dit chapeau ! pour la '''tâche''' accomplie et non pas chapeau ! pour la '''tache''' sur le vêtement.
* Un chien ou un chat marche sur deux paires de pa'''tt'''es. Par contre, on fait cuire des p'''ât'''es dans une casserole qu'on couvre avec le chapeau du â.
* "Traîner ses guêtres", c'est flâner.
<u>Pour les anglophones</u>, il suffit souvent de comparer le mot anglais de même racine que le mot français sur lequel on a un doute pour l'accent circonflexe. Si ce mot anglais contient un S, le mot français équivalent contient souvent un accent circonflexe. Exemples :
* Ancêtre / Ancestor
* Apôtre / Apostle
* Arrêt / Arrest
* Bâtard / Bastard
* Bête / Beast
* Boîte / Box
* Château / Castle
*Cloître / Cloister
* Côte (anatomie, rivage, pente, culinaire) / Coast (rivage)
* Coût / Cost
* Crête / Crest (vague, cimier, huppe…)
* Dégoût / Disgust
* Épitre / Epistle
* Fête, Festif (fr) / Feast (eng)
* Guêpe / Wasp (eng) tous deux venant de vespa (latin)
* Forêt / Forest
* Hâtif / Hasty
* Hôpital / Hospital
* Hôte, hôtesse / Host, hostess
* Hâte / Haste
* Honnête / Honest
* Huître / Oyster
* Île / Island
* Intérêt / Interest
* Maître / Master
* Mât / Mast (bateau)
* Paître / To pasture
* Pâtisserie, Pâte / Pastry / Pasta (ital.)
* Plâtre / Plaster
* Quête / Quest
* Rôtir / To roast
* Tâche (travail et non salissure) / Task
* Tempête / Tempest
===<u>Pluriel</u>===
*Pluriel en OUX au lieu de OUS
::Un '''hibou''' moche comme un '''pou'''
::Avait pour '''joujou''' sur ses '''genoux'''
::Un '''caillou''' aussi '''chou''' qu’un '''bijou'''.
Variante :
::Viens mon '''chou''', mon '''bijou'''
::Viens sur mes '''genoux'''
::Avec des '''joujoux''' et des '''cailloux'''
::Pour éloigner ces vilains '''hiboux''' pleins de '''poux'''
Variante :
::Viens mon '''chou''', mon '''joujou''', mon '''bijou'''
::Sur mes '''genoux'''
::Jeter des '''cailloux'''
::À ces vieux '''hiboux''', pleins de '''poux'''
Variante :<blockquote>Viens mon '''chou''', sur mes '''genoux''' avec tes '''joujoux''' et tes '''bijoux'''</blockquote><blockquote>Pour jeter des '''cailloux''' sur les vilains '''hiboux''' pleins de '''poux'''.</blockquote>
Variante :
Répéter plusieurs fois très vite : Hi-ge-jou-bi-ca-chou-pou.
Vous avez ainsi les premières syllabes des 7 noms qui se terminent en "oux" au pluriel.
2e variante:
'''J'''e '''P'''eux '''B'''oire '''C'''omme '''C'''es '''G'''ros '''H'''ommes.
* Les noms terminés en « -al » font leur pluriel en « -aux » (sauf ''aval, bal, cal, carnaval, chacal, choral, festival, mistral, naval, pal, récital, régal''… qui font leur pluriel en « s ») :
::Dans mon pays '''natal'''
::Où les gens sont pourtant '''joviaux'''
::Eut lieu, c’était '''fatal''', un combat '''naval''',
::Heureusement, ce fut le combat '''final'''
::Parce qu’il faisait '''glacial'''.
== [[w:Grammaire|Langues étrangères]] ==
Ces langues nous sont étrangères, d’où l’importance de trouver des moyens mnémotechniques
=== [[w:Allemand|Allemand]] ===
==== Liste des particules verbales non détachables ====
''J’ai mis Cerbère en enfer'' : ge-, miss-, zer-, be-, er-, ent-, emp-, ver-
''Cerbère gémit en enfer'' : zer-, be-, er-, ge-, miss-, ent-, emp-, ver-
''Miss Verzer bégaie en panthère'' : miss-, ver-, zer-, be-, ge-, emp-, ent-, er-
==== Genre des mots ====
Les mots (de plus d'une syllabe) se terminant en -e, -ei, -ie, -heit, -keit, -tion, -ung sont féminins. <br />
Il existe bien sûr des exceptions : der Däne, das Genie, der Ursprung, der Hochsprung...
=== [[w:Anglais|Anglais]] ===
==== Mots contraires ou confondables ====
* Left: gauche / '''R'''ight: d'''r'''oite
** avec la''' main gauche''', on peut former un '''L''' en tenant les doigts en haut et le pouce en avant. C’est le '''L''' de '''L'''eft.
** dans l’alphabet le '''L''' est à '''gauche''' ('''L'''eft) et le '''R''' est à '''droite''' ('''R'''ight) :
**:A B C D E F G H I J K '''L''' M N O P Q '''R''' S T U V W X Y Z
** Copy'''right''' veut dire '''droit''' d'auteur.
** Quand on est a'''droit''', c’est bien (= '''right''' en anglais)
* Odd (3 lettres) : impair / Even (4 lettres) : pair
** Tuesday : mardi / Thu'''r'''sday : jeudi
*** Thu'''r'''sday est le quat'''r'''ième jour de la semaine, il a donc un '''r''' ('''quatrième''' lettre)
*** En classant les mots dans l’ordre lexicographique :
****jeudi (Thursday) est avant mardi (Tuesday),
****Thursday (jeudi) est avant Tuesday (mardi).
*** Je'''u'''di et Th'''u'''rsday ont tous les deux la lettre '''u''' en troisième position.
*** étymologiquement : Thursday = jour de '''Thor''', et jeudi = jour de '''Jupiter''' (Jovis die). Thor (mythologie nordique) et Jupiter (mythologie romaine) sont tous les deux ''dieu du tonnerre''. Même chose pour l'étymologie de Tuesday (jour de '''Tyr''') et de mardi (jour de '''Mars'''), tous les deux étant ''dieu de la guerre''. Mais il est plus difficile de retrouver Odin dans Tuesday.
***TUEsday sonne comme two-sday two étant égal au nombre 2 et mardi est le deuxième jour de la semaine.
=== [[w:Espagnol|Espagnol]] ===
==== [[w:Consonne|Consonnes]] doublées ====
Les seules consonnes que l’on peut trouver à l’écrit en double sont celles du mot CaRoLiNa.
On peut remarquer que le "[[w:LL|ll]]" est une consonne à part entière.
Attention ! Ne pas confondre N et Ñ
==== <u>Subjonctif</u> ====
===== verbe Savoir (saber) =====
si tu '''sé '''ton présent mais que tu ne '''sepa '''ton subjonctif ce n'est pas grave!
=== [[w:Japonais|Japonais]] ===
{{article détaillé|Japonais/Hiragana/Leçon 1}}
=== [[w:Néerlandais|Néerlandais]] ===
==== Liste des particules verbales non détachables ====
''begeherontverer' : be-, ge-, her-, ont-, ver-, er-''
BEnoit et Gerard ONT HERité du VERgER
==== Conjugaison de l’[[w:imparfait|imparfait]] ====
On forme l’[[w:imparfait|imparfait]] avec un '''t''' si le radical (Verbe -EN) du verbe se termine par F, K, P, S, T, CH.<br>
Retenez : '''F'''ran'''K'''lin '''p'''rend '''s'''on '''t'''hé '''ch'''aud. <br>
Si le radical se termine par une autre lettre, on forme l’imparfait avec un '''d'''.
Exemples : <br>
- pakken (''prendre'') : pak'''k'''-en > hij pak'''t'''e (''il prenait'')<br>
- ruilen (''échanger'') : rui'''l'''-en > hij ruil'''d'''e (''il échangeait'')
=== [[w:Russe|Russe]]===
==== Verbes à voyelle alternante ы/о dans le thème ====
'''К'''арл '''Р'''о'''М'''а'''Н'''о'''В'''
крыть « couvrir » - рыть « creuser » - мыть « laver » - ныть « gémir; faire mal » - выть « hurler »
Se fléchissent tous sur le modèle :
* infinitif : мыть (accent stable au passé: м'ыла)
* conjugaison : мóю, мóешь... мóют
Contrairement à ст'ыть, ст'ыну « refroidir » ; слыть, слывý « être réputé... »
== Littérature ==
=== Auteurs français du {{XVIIe siècle}} ===
''Sur une racine de la bruyère, une corneille boit l’eau de la fontaine Molière''
([[w:Jean Racine|Racine]], [[w:Jean de La Bruyère|Jean de La Bruyère]], [[w:Pierre Corneille|Pierre Corneille]], [[w:Nicolas Boileau|Nicolas Boileau]], [[w:Jean de La Fontaine|Jean de La Fontaine]], [[w:Molière|Molière]])
Variante : La Corneille perchée sur la Racine de La Bruyère, Boileau de La Fontaine Molière
''(Remarque : La fontaine Molière est une fontaine à Paris)''
== Théâtre ==
Face à la scène, le côté '''j'''ardin et le côté '''c'''our sont du côté de chaque initiale de '''J'''ésus '''C'''hrist, de '''J'''ules '''C'''ésar , de '''J'''acques '''C'''artier ou de '''J'''acques '''C'''hirac (J.C. : jardin à gauche, cour à droite)
Face au public, c’est l’inverse, et le côté '''cour''' est le côté du [[w:cœur|cœur]], à gauche.
== Musique ==
'''Ah ! Lala !'''
* Se souvenir de cette interjection pour faire correspondre les notes musicales latines (do, ré, mi fa...) avec les anglo-saxonnes (C, D, E, F...) '''A''' correspond à '''la''', il n'y a plus qu'à suivre B=si, C=do, D=ré, E=mi, F=fa, G=sol.
'''TS MS DSS'''
*''Nom des degrés'': '''t'''onique, '''s'''us-tonique, '''m'''édiante, '''s'''ous-dominante, '''d'''ominante, '''s'''us-dominante, '''s'''ensible
'''Sa mère la racaille ! Saleté de fumier !'''
*''Ordre des bémols'' : '''S'''i '''m'''i '''l'''a '''r'''é '''s'''ol '''d'''o '''f'''a
'''Six mille laquais repus songent au dodo, fatigués'''
*''Ordre des bémols'' : '''Si''' '''mi''' '''la''' '''ré''' '''so'''l '''do''' '''fa'''
'''Facteur, donne au soldat réjoui la missive'''
*''Ordre des dièses'' : '''Fa''' '''do''' '''sol''' '''ré''' '''la''' '''mi''' '''si'''
'''Dommage, la mine est cassée Do Majeur --> La mineur'''
*"Gamme relative" de Do Majeur
'''Il Doit Posséder Les Modes En Lui'''
Ionien, Dorien, Phrygien, Lydien, Mixolydien, Éolien, Locrien
== Géographie ==
=== Points cardinaux ===
==== Où est l’est ? ====
*Visualiser Strasbourg et Brest. Strasbourg est à l'est, Brest est à l'ouest
* Penser au mot '''O'''rang'''E''', sur une carte, l'Ouest est à gauche et l'Est à droite.
* Écrire "'''où est''' l''''est'''" --> ouest à gauche et est à droite (en considérant le nord en haut bien entendu)
* L’ouest est à gauche, l’est à droite (si le nord est au-dessus).
* Penser que si on regarde la France, à gauche c'est l' ''eau'' comme dans '''O'''uest et à droite c'est l' ''étranger'' comme dans '''E'''st.
* Penser qu'en France, on parle des "pays de l'est" (à droite sur la carte) et en parlant de la conquête de l'ouest on pense à l'Amérique (à gauche sur la carte)
* Penser à : Ouest, le suffixe "est" se trouve à droite, tout comme l’est. Ce qui signifie "ouest" à gauche et "est" à droite.
* Écrire ONE (1 en anglais) : Ouest-Nord-Est
* Penser au mot 'OiE': l’Ouest est à gauche comme le O et l’Est est à droite comme le E (si le Nord est en haut)
* Dans le mot ouest il y a un "u" comme dans gauche. Dans le mot est il n'y a pas de "u" comme dans droite.
* Retenir le mot NESO en tournant dans le sens des aiguilles d'une montre, car l'inverse donne la nausée (NOSE).
==== Le soleil se couche à l’est ou à l’ouest ? ====
* Penser qu’en France, on voit de beaux couchers de soleil sur nos côtes atlantiques, à l’ouest.
* Penser aussi au pays du soleil levant, le Japon, qui est bien à l’est du continent
* Ou encore : Le Soleil se l'''è'''ve à l’'''e'''st et se c'''ou'''che à l’'''ou'''est
=== Pays limitrophes de la France ===
Aime '''I''S''A''B''E''L''A''' (Aimer pour la lettre '''M''')
'''M'''onaco, '''I'''talie, '''S'''uisse, '''A'''llemagne, '''B'''elgique, '''E'''spagne, '''L'''uxembourg, '''A'''ndorre.
''MAL BAISÉ''
'''M'''onaco, '''A'''ndorre, '''L'''uxembourg, '''B'''elgique, '''A'''llemagne, '''I'''talie, '''S'''uisse, '''E'''spagne.
''AIMABLES''
'''A'''llemagne, '''I'''talie, '''M'''onaco, '''A'''ndorre, '''B'''elgique, '''L'''uxembourg, '''E'''spagne, '''S'''uisse
Avec l’océan Atlantique, la Manche et la Méditerranée en plus :
'' '''O'''h '''MA''' '''MER'''veilleuse '''BALISE''' ''
'''O'''h = '''O'''céan Atlantique '''MA'''='''MA'''nche '''MER'''= '''MER'''Méditerranée '''BALISE'''= '''B'''elgique '''A'''llemagne '''L'''uxembourg '''I'''talie '''S'''uisse '''E'''spagne
PS cette liste est valable seulement pour la France Métropolitaine car le pays avec lequel la France a la plus longue frontière est le ... Brésil ! (car la Guyane est un département français)
=== Grands lacs de l'Amérique du Nord ===
''SMHEOL'' (d'ouest en est)
'''S'''upérieur, '''M'''ichigan, '''H'''uron, '''E'''rié, '''O'''ntario (et St '''L'''aurent)
''HOLMES (élémentaire !)''
'''H'''uron, '''O'''ntario, St '''L'''aurent, '''M'''ichigan, '''E'''rié, '''S'''upérieur.
Une autre méthode, souvent enseignée dans les cours de géographie, fait appel au mot anglais ''foyers'' de la manière suivante :
''HOMES''
'''H'''uron, '''O'''ntario, '''M'''ichigan, '''E'''rié, '''S'''upérieur
=== Les 5 arrondissements de New-York (Boroughs) ===
Vous n'arrivez pas à vous souvenir des 5 arrondissements de la grande ville de New-York? En sachant qu'il est incontournable d'aller se promener dans les grandes avenues et rues sans s'arrêter dans un stand BBQ et y manger les bons hot-dogs d'un sympathique New-yorkais. Il faut dire: '''Si Man BBQ''' (Staten Island, Manhattan, Brooklyn, Bronx, Queen). Bon appétit, bonne visite!.
=== Pays baltes ===
Vous confondez les pays baltes sur la carte ? C'est tout simple, ils sont placés par ordre alphabétique du nord au sud... [[w:Estonie|Estonie]], [[w:Lettonie|Lettonie]], [[w:Lituanie|Lituanie]]
* Cela fonctionne aussi avec les appellations anglo-saxonnes : Estonia, Latvia, Lithuania et aussi avec les noms locaux : Eesti, Latvija, Lietuva.
* Pour les capitales de ces pays : Estonie [[w:Tallinn|Tallinn]], Lettonie [[w:Riga|Riga]] et Lituanie [[w:Vilnius|Vilnius]]
'''T'''rafic '''R'''outier '''V'''olumineux
=== Les tropiques ===
Les tropiques du Cancer et du Capricorne sont classés du nord au sud par ordre alphabétique.
Le capricorne coule (en bas) car il a plus de lettres, il est plus lourd. L'antarctique aussi : l'arctique flotte.
ou tropique du caNcer : N represente le Nord.
ou Capricorne sonne comme "Cap Horn" donc au Sud
=== Les pays d'Amérique Centrale ===
*Du nord au sud : [[w:Bélize|Bélize]], [[w:Guatemala|Guatemala]], [[w:Honduras|Honduras]], [[w:Salvador|Salvador]], [[w:Nicaragua|Nicaragua]], [[w:Costa Rica|Costa Rica]], [[w:Panama|Panama]].
BGHSNCP soit: '''B'''eau '''G'''arçon '''H'''abitant '''S'''alvador '''N'''ettoie et '''C'''i'''r'''e les '''P'''lanchers ou Belle Guatemalaise Habitant Salvador, Nage sur la Côte du Panama.
=== Fleuves de Russie ===
D'ouest en est, les initiales des cinq principaux fleuves de [[w:Russie|Russie]] forment le mot "VOILA" : [[w:Volga|Volga]], [[w:Ob|Ob]], [[w:Ienisseï|Ienisseï]], [[w:Léna|Léna]], [[w:Amour (fleuve)|Amour]].
L'Ob, le Ienissei et la Léna sont les trois plus grands cours d'eau de Sibérie.
=== Pays du Moyen-Orient ===
Le Qatar est une presQu'île dans la péninsule arabiQue. Le Qatar peut être vu comme comme une Crête de CoQ juchée sur l'Arabie saoudite et s'ouvrant sur le golfe arabo-persiQue.
=== Pays d'Asie Centrale (en -stan) ===
* Du Nord au Sud et de l'Ouest à l'Est
'''Kaz'''akhstan - '''Ou'''zbékistan - '''Ki'''rghizistan - '''Tu'''rkménistan - '''Ta'''djikistan - '''Af'''ghanistan - '''Pa'''kistan
Kaz Ou Ki Tu Ta Af Pa
Kazouki, tu taffes pas ?
=== Principales villes traversées par la Loire ===
* De l´Atlantique au Mont Gerbier de Jonc
'''Na'''thalie '''an'''goisse '''to'''ujours les '''bl'''ondes '''or'''iginaires de '''Nevers''', elles '''ro'''ugissent '''sa'''ns '''pu'''deur.
Nantes Angers Tours Blois Orléans Nevers Roanne Saint-Étienne Le Puy en Velay
== Histoire ==
=== Préhistoire ===
{{loupe|#Les périodes géologiques de l’ère primaire|# Les périodes géologiques de l’ère secondaire}}
==== Évolution des [[w:Homininae|homininés]] ====
Les '''Austral'''iens '''habil'''es eurent une '''érec'''tion, quand ils aperçurent, dans le '''néan'''t, des '''sapins''' gigantesques, <br /> ce qui donne, par ordre d'apparition<br />
[[w:Australopithèque|Australopithèque]], [[w:Homo habilis|Homo habilis]], [[w:Homo erectus|Homo erectus]], [[w:Homme de Néanderthal|Homme de Néanderthal]] et [[w:Homo Sapiens|Homo Sapiens]].
=== Les 7 Merveilles du monde antique ===
"'''Mostapha''' ! '''J’attends''' la '''copie''' !"
Variante: "'''Mostapha''' ! '''J’attends''' ta '''coloscopie''' !"
('''Mau'''solée d’Halicarnasse, '''Sta'''tue de Zeus à Olympie, '''Pha'''re d’Alexandrie, '''Ja'''rdins suspendus de Babylone, '''Tem'''ple d'Artémis à Éphèse, '''Co'''losse de Rhodes, '''Py'''ramides d’Égypte)
=== Les 7 rois de Rome ===
''Ronutuann' tarsertar'' (qu'on retient mieux en imaginant le paresseux boucher Ronu : "Ronu, tuant tard, sert tard")
('''Ro'''mulus, '''Nu'''ma Pompilius, '''Tu'''llus Hostilius, '''An'''cus Martius, '''Tar'''quin l’Ancien, '''Ser'''vius Tullius, '''Tar'''quin le Superbe)
=== Les 11 [[w:Liste des empereurs romains|premiers empereurs romains]], dans l’ordre de leur règne ===
''AuTiCaClauNéGalOViVesTiDo''
('''Au'''guste, '''Ti'''bère, '''Ca'''ligula, '''Clau'''de, '''Né'''ron, '''Gal'''ba, '''O'''thon, '''Vi'''tellus, '''Ves'''pasien, '''Ti'''tus, '''Do'''mitien)
Et les six suivants : ''NeTraHadAnMarCo''
(Nerva, Trajan, Hadrien, Antonin, Marc-Aurèle, Commode)
Cesautica
Clonegalo
Vivestido
CESar, AUguste, TIbere, CAligula
CLAUde, NEron, GALba, Othon
VItellus, VESpasien, TItus, Domitien
=== Les traités napoléoniens, dans l’ordre de leur signature ===
'''''CAV''''' (penser à une ''cave'') : ''Cambalu, Apresti, Viparis''
('''Cam'''po Formio, '''Bâ'''le, '''Lu'''néville, '''A'''miens, '''Pres'''bourg, '''Ti'''lsit, '''Vi'''enne, '''Paris''')
=== Les présidents de la troisième République française ===
''Thimagré Carcafauloufa Poindemidoudoule''
('''Thi'''ers, '''Ma'''c-Mahon, '''Gré'''vy, '''Car'''not, '''Ca'''simir-Perier, '''Fau'''re, '''Lou'''bet, '''Fa'''llières,
'''Poin'''caré, '''De'''schanel, '''Mi'''llerand, '''Dou'''mergue, '''Dou'''mer, '''Le'''brun)
OU ce petit poème :
Tire Mon Glaive
Car Casse-Pierre Fort
Loup Faillit Point
Dèche Mille Dômes
D'où Merle Brun
=== Les présidents de la cinquième République française ===
'''D'''es '''P'''illards '''G'''ouvernent, '''M'''ais '''C'''hacun '''S'''ubit '''H'''élas la Macronie
'''D'''ur '''P'''armesan '''G'''orgonzola '''M'''ozzarella '''Ch'''auds '''S'''ervis.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande.
'''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux et '''M'''alades.
<math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande, Macron.
- Le sauveur de la France, Charles '''de Gaulle''', s’orthographie avec deux L. On peut retenir que de Gaulle a deux L, comme les deux barres de la croix de Lorraine, symbole de la France libre.
On retient que la Gaule n’a qu’un L comme le L unique dans Celtes. Car "les Gaulois" est le nom que Jules César donne aux Celtes.
=== Les présidents américains à partir de Roosevelt ===
'''R'''udy '''T'''ente '''E'''n '''K'''araté '''J'''e '''N'''ique '''F'''abienne '''C'''omme '''R'''udy '''B'''ouche '''C'''ontre '''B'''ouche '''O'''utré '''T'''errifié.
'''R'''oosevelt '''T'''rouva '''E'''léonore en '''K'''imono, '''J'''ames '''N'''e '''F'''ilma '''C'''arrément '''R'''ien, '''B'''rave '''C'''améraman '''B'''ouché et '''T'''êtu !
<math>\Longrightarrow</math>Roosevelt, Truman, Eisenhower, Kennedy, Johnson, Nixon, Ford, Carter, Reagan, Bush, Clinton, Bush, Obama, Trump.
=== Les dirigeants de l'URSS et de la Russie ===
'''L'''aissant '''S'''on '''K'''imono '''B'''leu '''À''' '''T'''rois '''G'''amins, '''E'''lle '''P'''ut '''M'''aintenir '''P'''outine.
L = Lénine, S = Staline, K = Khrouchtchev B = Brejnev, À = Andropov, T = Tchernenko, G = Gorbatchev.....la "virgule" marque la chute du communisme, et E = Eltsine, P = Poutine, M = Medvedev, P = Poutine
== Médecine ==
=== Plan des muscles complexus ===
1. Muscle semi-épineux de la tête
* Sème tranquillement 156 graines dans un carré de terre pour 71 arbres épineux.
(Le semi-épineux a pour origine les processus transverses de Th1 à TH5/Th6 et C4 à C7 et se termine sur les processus épineux de C7 ) th1)
2. Muscle longicissimus du cou
*Louons tranquillement une sainte tu (la) sauteras.
(Le muscle longicissimus du cou a pour origine les processus transverses de Th1 à Th5 et se termine sur les tubercules post de C3 à C7)
=== Les os du carpe ===
*''SSPP - TTCC'' (Initiales)
* ''Sca-Lu-Py-Pi T-T-Go-Oc'' (Phonétique)
<math>\Longrightarrow</math>('''Sca'''phoïde, (Semi-'''Lu'''naire)'''lu'''natum* , '''Py'''ramidal, '''Pi'''siforme - '''T'''rapèze, '''T'''rapézoïde, '''C'''apitatum ''', '''os '''C'''rochu ''')'''
NB : dans la nouvelle nomenclature ce n'est plus le semi-lunaire mais le LUNATUM
*On peut le voir sous un autre angle :
** PI - TRI - LU - SCA
*: (pisciforme) (triquetrum) (lunatum) (scaphoide)
** HA - CA - TRI - TRA
*: (hamatum) (capitatum) (trapézoide) (trapèze)
Ou encore prendre les consonnes de ces 2 mots :
*'''P'''é'''T'''a'''L'''e'''S''' : '''P'''isiforme - '''T'''riquetrum - '''L'''unatum - '''S'''caphoïde
* a'''TT'''a'''CH'''e : '''T'''rapèze - '''T'''rapézoïde - '''C'''apitatum - '''H'''amatum
*Trouvé par un étudiant :
*
** '''S'''a'''L'''e '''T'''e'''P'''u, '''<nowiki>T'</nowiki>'''é'''T'''ais à '''CH'''ier.
** '''S'''uce '''l'''a '''t'''rique '''P'''atrick, '''t'''u '''t'''ireras '''Ch'''arlotte
** Le '''S'''carabée à '''L'''unettes '''T'''rie ses '''P'''ièces, '''T'''out '''T'''as est un '''C'''apital ('''h''')Amassé"
** '''S'''a'''L'''u'''T''' '''P'''ierre, '''T'''<nowiki/>'é'''T'''ais '''C'''haud '''H'''ier.
=== Les muscles épicondyliens médiaux (ex-épitrochléens) du membre supérieur ===
''Grand Papa cuve et ronfle''
<math>\Longrightarrow</math>('''Grand pa'''lmaire, Petit '''pa'''lmaire, '''Cu'''bitus antérieur, '''Ron'''d pronateur, '''Flé'''chisseur commun superficiel)
Une autre phrase est proposée
"Grand Papa, Petit Papa, fléchit rondement le cul en avant"
<u>Avec la nouvelle nomenclature</u> :
''Paulo Fuck Les Filles Sans Défense Faisant Un CAprice''
''==> rond '''P'''ronateur, '''F'''léchisseur radial du carpe, '''L'''ong palmaire, '''F'''léchisseur '''S'''uperficiel des '''D'''oigts, '''Fl'''échisseur '''U'''lnaire du '''Ca'''rpe''
=== Les muscles épicondyliens latéraux (ex-épicondyliens) du membre supérieur ===
<math>\Longrightarrow</math>'''2'''ème '''Ra'''dial, '''Ext'''enseur '''commun''', '''ext'''enseur '''propre''' '''du 5'''ème doigt, '''Court Su'''pinateur, '''Cu'''bital '''Post'''érieur, '''Anconé'''
Deux rats excommuniés, expropriés du 5e ont cousu (court supinateur) le cul de la postière en cône.
<u>Avec la nouvelle nomenclature</u> :
''Charlie Rêve d'Explorer Des Etoiles, 5 Etoiles Uniques Au Sanctuaire''
''==>'' Court extenseur Radial du carpe, Extenseur commun des Doigts, Extenseur du 5ème doigt, Extenseur Ulnaire du carpe, Anconé, Supinateur
=== Les 12 paires de nerfs crâniens ===
==== Ancienne nomenclature ====
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''héière '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''rogs''
*'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''hérèse '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''osses''
<math>\Longrightarrow</math>'''O'''lfactifs, '''O'''ptiques, '''M'''oteur oculaire commun, '''p'''athétiques, '''T'''rijumeau, '''M'''oteur oculaire externe, '''F'''aciaux, '''A'''uditifs, '''G'''losso-pharyngiens, '''P'''neumogastriques, '''S'''pinaux, '''G'''rand hypoglosse.
* Nouvelle nomenclature
*'''''OL'''ivia '''OPT'''<nowiki>e pour l'</nowiki>'''OC'''éan c'est '''TRO'''p '''TRI'''<nowiki>ste d'</nowiki>'''A'''ller '''FA'''ire des '''V'''isites '''G'''avantes quand les '''VAGUES''' '''A'''<nowiki>pportent l'</nowiki>'''HYP'''nose.''
*'''O'''h '''O'''h ! '''O'''scar ! '''T'''a '''T'''héière '''A''' '''F'''ait '''V'''ingt '''G'''rands '''V'''erres '''A''' '''H'''ector.
*'' '''Ol'''ivier '''Op'''oil '''Ocul''' '''Troqu'''a '''Tri'''stement '''A'''vec '''Fa'''nny '''V'''ingt '''Gloss Par'''fums '''Va'''nille '''Accessoire'''<nowiki> d'</nowiki>'''Hy'''dratation.''
*'''''Ol'''é '''O'''<nowiki>scar d'</nowiki>'''Occ'''ident ! '''Tr'''availle ton '''Tri'''ceps, tes '''Abd'''o et tes '''F'''esses au '''WC'''. '''Gl'''isse '''vague'''ment ton '''accessoire''' et '''hip''' !''
*'''''O'''yez, '''O'''yez ! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu, '''G'''rand '''V'''antard '''A''' '''H'''onte.''
*'''''O'''yé! '''O'''yé! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu (la) '''G'''rande '''V'''ague '''À''' '''H'''awaii''.
*'' '''Ol'''af '''Opt'''a '''Occ'''asionellement pour le '''Tro'''quet '''T'''andis qu' '''Abd'''el '''Fa'''isait '''V'''alser '''G'''rand'''-P'''ère '''Vague'''ment '''A'''utour de l' '''Hippo'''campe. ''
*'''''O'''n '''O'''ccasion '''O'''livier '''T'''ries '''T'''o '''A'''nally '''F'''uck '''V'''arious '''G'''uys, '''V'''aginas '''A'''re '''H'''istory''.
*'''''Ol'''a! '''Op'''hélie '''O cul Tro'''p '''Tri'''pant '''A Fa'''it '''Co'''quettement '''Glo'''usser '''Va'''lentin '''A''' l'<nowiki/>'''Hypo'''drome.''
<math>\Longrightarrow</math>'''O'''lfactif, '''O'''ptique, '''O'''cculomoteur, '''T'''rochléaire, '''T'''rijumeau, '''A'''bducens, '''F'''acial, '''V'''estibulo-Cochléaire ''ou'' '''C'''ochléo-vestibulaire, '''G'''losso-Pharyngien, '''V'''ague, '''A'''ccessoire, '''H'''ypoglosse.
==== Sensitif ou moteur ====
- Un dernier pour savoir la '''composante de chaque nerfs''' :
Mots commençant par un '''S''' = sensitif, '''M''' = moteur, '''B''' = les deux (both). Ensuite les noms communs et les adjectifs sont parasympathiques (Money, brother, big, boobs).
*'''''S'''ome '''S'''ay '''M'''oney '''M'''atters, '''B'''ut '''M'''y '''B'''rother '''S'''ays : '''B'''ig '''B'''oobs '''M'''atter '''M'''ost.''
ex: Boobs = 10e nerf (Vague), B = sensitif et moteur, nom commun = parasympathique. Ceci correspond aux caractéristiques du nerf vague !
- Une autre plus rigolote et moins décente :
*'''''S'''eb '''S'''uces '''M'''oi '''M'''es '''D'''eux '''M'''amelons '''D'''e '''S'''ilicone '''D'''é-'''D'''é '''M'''e '''M'''anque''
'''S'''ahara '''S'''ablonneux (et) '''M'''er '''M'''orte, '''D'''eux '''M'''ondes '''D'''e '''S'''ilence (et) '''D'''éserts '''D'''e '''M'''ouvants '''M'''irages
'''S''' = sensitif,
'''M''' = moteur,
'''D'''= les deux.
Ainsi le 1{{er}} nerf crânien est sensitif.
Le 9{{e}} et le 10{{e}} sont à la fois sensitifs et moteurs, etc.
=== Les 15 collatérales de l’artère maxillaire ===
'''''T'''on '''m'''épris '''p'''eut '''a'''mener '''m'''a '''t'''empête '''p'''etite '''b'''iche '''t'''ant aimée. '''Un''' '''p'''etit '''c'''âlin '''p'''eut '''p'''ardonner''
<math>\Longrightarrow</math>('''T'''ympanique, '''M'''éningée moyenne, '''P'''etite méningée, '''A'''lvéolaire inférieure, '''M'''asseterine, '''T'''emporale profonde postérieure, '''P'''térygoïdienne, '''B'''uccale, '''T'''emporale profonde antérieure, '''A'''lvéolaire supérieure, '''In'''fra-orbitaire, '''P'''alatine descendante, du '''C'''anal ptérygoïdien, '''P'''térygo-palatine, '''P'''haryngienne)
(N.B. Non! l'artère pharyngienne est une branche de la carotide externe!)
les 15 branches dans l'ordre:
un '''T'''ic '''MENING'''é '''PE'''ut '''DE'''venir '''MA'''léfique, '''T'''andis qu'un '''BU'''bon '''TE'''rriblement '''AL'''gique '''P'''eu'''T''' être '''SOU'''lagé '''VI'''te '''PA'''r une '''PT'''yaline '''SP'''écifique
'''( T'''ympanique, '''MENINGE'''é moyenne, '''PE'''tite méningée, '''DE'''ntaire inférieure, '''MA'''ssétérine, '''T'''emporale profonde moyenne, '''BU'''ccale, '''TE'''mporale profonde antérieure, '''AL'''véolaire, '''PT'''érygoïdienne, '''SOU'''s orbitaire, '''VI'''dienne, '''PA'''latine descendante, '''PT'''érygopalatine, '''SP'''hénopalatine.)
=== Les branches de l'artère axillaire ===
'''TH'''éodore '''a''' '''m'''angé '''s'''on '''c'''a'''c'''a
<math>\Longrightarrow</math>'''TH'''oracique supérieure, '''A'''cromiothoracique, '''M'''ammaire Interne, '''S'''capulaire et les deux '''C'''irconflexes
Cette artère se trouve dans la région de l'aisselle et pas ailleurs.
=== Branche de l'artère carotide externe : ===
'''''T'''ous '''L'''es '''F'''rançais '''O'''nt '''A'''pplaudi le '''P'''résident '''M'''onsieur '''T'''hiers''.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''P'''haryngienne ascendante, '''M'''axillaire interne, '''T'''emporale superficielle):
=== Collatérales de la carotide externe ===
'''''T'''ire '''l'''a '''f'''icelle, '''p'''ortier ! '''O'''uvre '''à''' '''t'''on '''m'''aître '''r'''apidement''
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''P'''haryngienne ascendante, '''O'''ccipitale, '''A'''uriculaire postérieure, '''T'''emporale superficielle, '''M'''axillaire, '''R'''ameau parotidien)
'''T'''ou'''s''' '''l'''es '''F'''rançais '''a'''cclament '''O'''bama '''p'''résident
<math>\Longrightarrow</math>('''T'''hyroïdienne '''s'''upérieure, '''L'''inguale, '''F'''aciale, '''A'''uriculaire postérieure, '''O'''ccipitale, '''P'''haryngienne ascendante)
'''S'''ome '''a'''ngry '''l'''ady '''f'''igured '''o'''ut '''p'''ost '''m'''enopausal '''s'''yndrom.
<math>\Longrightarrow</math>('''S'''uperior thyroidal, '''A'''scending pharyngeal, '''L'''ingual, '''F'''acial, '''O'''ccipital, '''P'''osterior auricular, '''M'''axillary, '''S'''uperficial temporal)
Ce dernier, bien qu'en anglais, a l'avantage d'indiquer les artères dans l'ordre ascendant.
'''T'''u '''P'''eux '''L'''a '''F'''ourrer '''O'''u l'''A''' '''M'''anger '''T'''oute.
<math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''P'''haryngée ascendante, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''M'''axillaire, '''T'''emporale superficielle)
Ce dernier est une variante québécoise à caractère sexuel qui a également l'avantage d'indiquer les artères dans l'ordre ascendant.
=== Les collatérales de l’artère ophtalmique ===
'''''R'''emets '''l'''es '''c'''apotes '''s'''ans '''n'''ous '''f'''aire '''p'''<nowiki>erdre l'</nowiki>'''é'''rection''
<math>\Longrightarrow</math>(Centrale de la '''R'''étine, '''L'''acrymales, '''C'''iliaires, '''S'''upra-orbitaire, '''N'''asales, '''F'''rontales, '''P'''alpébrales, '''E'''htmoïdales antérieures et postérieures)
=== Les rameaux du plexus lombaire ===
'''Il''' '''hyp'''notise '''il'''lico l''''igu'''ane '''gé'''ant et '''fé'''roce et lui '''c'''oupe '''l'''ittéralement '''la''' '''cuisse''' qui '''fai'''sait '''ob'''struction '''car''' elle '''l'omb'''rageait.
<math>\Longrightarrow</math>('''il'''io-'''hyp'''ogastrique; '''il'''io-'''ingu'''inal; '''gé'''nito-'''fé'''moral; '''c'''utané '''l'''atéral de '''la''' '''cuisse'''; '''fé'''moral; '''ob'''turateur; du muscle '''car'''ré des '''lombes''')
=== Les rameaux du plexus sacré ===
'''Si''' un '''gl'''acier '''sup'''er '''inf'''idèle est '''honteux''' d’avoir '''per'''foré le '''rect'''um et '''élev'''é '''l’anus''' d’un '''cu'''isinier '''po'''urtant '''c'''onsentant, un '''curé''' '''fe'''ra un '''ju'''gement '''im'''partial en '''oub'''liant l’'''in'''acceptable.
<math>\Longrightarrow</math>('''sci'''atique; '''gl'''utéal '''sup'''érieur et '''inf'''érieur; '''honteux'''; '''pir'''iforme; '''rect'''al supérieur ; '''élév'''ateur de '''l’anus'''; '''cu'''tané '''po'''stérieur de la '''c'''uisse; '''carré''' '''fé'''moral et '''ju'''meau '''in'''férieur; '''ob'''turateur '''in'''terne)
=== Les muscles du grand trochanter ===
Mon '''g'''ars, '''troqu'''ons: Le '''Petit''' '''Pierre''' et '''les jumeaux''' '''moyen'''nant '''fesses''' à '''obtur'''er.
<math>\Longrightarrow</math>('''G'''rand '''Troch'''anter: '''petit''' fessier; '''pir'''iforme; '''jumeaux''' supérieur et inférieur; '''moyen fess'''ier; '''obtur'''ateurs internes et externes)
=== Les muscles de la patte d'oie ===
*SA GRA - Tte ("ça gratte")
*# Sartorius (ex-Couturier)
*# Gracile
*# Tendineux
**CGT
**Sartre est grat et tendre
**ça gratte
=== Les ménisques du genou ===
'''''CI'''TR'''OE'''N''
(Le ménisque en forme de '''C''' est le ménisque '''I'''nterne ; le ménisque en forme de '''O''' est le ménisque '''E'''xterne)
=== Les obturateurs pelvi-trochantériens ===
Être '''ex-empt''' d''''im-pôt'''s
(L'obturateur '''ex'''terne s'insère à la face '''ant'''érieure de l'os coxal, l'obturateur '''in'''terne s'insère à la face '''po'''stérieure de l'os coxal)
== Mythologie ==
=== Les 3 Grâces ===
'''Aglaé''' offre une u'''sine''' à Na'''thalie''' (variante : Aglaé offre Rosine à Nathalie)
** Aglaé, Euphrosine et Thalie
=== Les 9 Muses ===
Voici l'astuce des étudiants en grec à l'école pour retenir les neuf muses :
'''''Cl'''ame, '''Eu'''gène, '''ta''' '''mél'''odie, '''terr'''ible '''air''' '''pol'''onais, o'''ura'''gan '''cal'''culé''
<math>\Longrightarrow</math>('''Cl'''io, '''Eu'''terpe, '''Tha'''lie, '''Mel'''pomène, '''Ter'''psichore, '''Ér'''ato, '''Pol'''ymnie, '''Ura'''nie, '''Cal'''liope)
Calliope porte une couronne d’or, Clio une couronne de laurier, Érato une couronne de myrtes et de roses, Euterpe une couronne de fleurs, Melpomène une couronne de pampre de vigne, Polymnie une couronne de perles, Terpsichore une couronne de guirlandes, Thalie une couronne de lierre, et Uranie une couronne d'étoiles
=== Les dieux grecs ===
''Hazah Phadhadah''
<math>\Longrightarrow</math>('''H'''éra, '''A'''phrodite, '''Z'''eus, '''A'''pollon, '''H'''éphaïstos, '''P'''oséidon, '''H'''ermès, '''A'''rtémis, '''D'''ionysos, '''H'''adès, '''A'''théna, '''D'''éméter, '''A'''rès, '''H'''estia)
Notons que la plupart des dieux grecs commencent par la lettre "A" ou "H".
{| class="wikitable"
|-
| Hestia || arrêta || d'aimer || Zeus<small>:</small>
|| possédée || <small>par</small> Dionysos<small>,</small>
|| <small>elle</small> erra || <small>dans</small> Athènes</small>,</small>
|| affolée<small>,</small> || <small>et fit</small> l'apologie || <small>de l'</small>art || effarant || d'Hermes || et d'Hadès
|-
| Hestia || Arès || Demeter || Zeus || Poséidon || Dionysos
|| Héra || Athéna || Aphrodite || Apollon || Artémis || Héphaïstos || Hermes || Hades
|}
=== Les dieux romains ===
'''''J'''eune '''v'''euve '''j'''oyeuse '''c'''herche '''v'''ieux '''b'''aron '''m'''ême '''m'''alade '''a'''fin '''d'''e '''v'''ivre '''m'''ieux'' '''p'''oint
<math>\Longrightarrow</math>('''J'''unon, '''V'''énus, '''J'''upiter, '''C'''érès, '''V'''ulcain, '''B'''acchus, '''M'''ercure, '''M'''inerve, '''A'''pollon, '''D'''iane, '''V'''esta, '''M'''ars, '''P'''luton)
== Religion ==
=== Les dimanches avant Pâques ===
Les petits protestants alsaciens (et allemands aussi, sans doute) apprenaient jadis le nom des dimanches qui précédaient Pâques grâce à la phrase : « '''I'''n '''R'''echter '''O'''rdnung '''L'''ehre '''J'''esu '''P'''assion », ce qui signifie : « Apprends dans le bon ordre la passion de Jésus ». On peut aussi dire : « '''I'''n '''R'''ektors '''O'''fen '''L'''iegen '''J'''unge '''P'''almen », ce qui veut dire : « Dans le poêle du recteur se trouvent de jeunes palmes (allusion à rameaux) ». Et ils retrouvaient : '''I'''nvocavit, '''R'''eminiscere, '''O'''culi, '''L'''aetare, '''J'''udica et '''P'''almarum (Palmsonntag - dimanche des Rameaux).
=== Péchés capitaux ===
Les initiales des [[w:sept péchés capitaux|sept péchés capitaux]] sont rassemblés dans le mot « Pô glacé ». ('''p'''aresse, '''o'''rgueil, '''g'''ourmandise, '''l'''uxure, '''a'''varice, '''c'''olère et '''e'''nvie)
** Autre expression pour retenir les 7 péchés capitaux : " CE GALOP ".
(Colère, Envie, Gourmandise, Avarice, Luxure, Orgueil, Paresse)
** L' '''ENV''' ie '''EN V'''eut, lorsqu'on lui demande de travailler, la '''P'''aresse se '''P'''ousse, l' '''O'''rgueil se pense plus haut ('''O''') que les autres, la '''G'''ourmandise mange du '''G'''ateau et du '''G'''ras, la '''L'''uxure va dans un hot-'''L''', l' '''A'''varice en '''A''', la '''C'''olère '''C'''rie.
**Ou encore une phrase: '''L'''es '''G'''rands '''E'''sprits '''O'''bligent '''C'''ertainement '''A''' '''P'''enser (luxure, gourmandise, envie, orgueil, colère, avarice, paresse)
(entendu cité par Jacques Lacarrière lors d'une interview sur France-Culture)' '
**Une phrase courte et très facile à mémoriser, car elle ne fait pas appel qu'aux initiales qui demanderaient encore beaucoup d'efforts pour les identifier. Ici, l'utilisation de mots qui agissent immédiatement sur la divulgation des 7 péchés: ''''<nowiki>Par goût, Colette envie l'orgue luxueux d'Avarice'</nowiki>''' ('''Par'''esse, '''gou'''rmandise, '''colè'''re, '''envie''', '''orgue'''il, '''lux'''ure, '''avarice''')
=== Épîtres de Paul ===
Rococo Galéphicol ThèThè TimTim TiPhilHé
('''Ro'''mains, 1 '''Co'''rinthiens, 2 '''Co'''rinthiens, '''Gal'''ates, '''Éph'''esiens,
'''Phi'''lippiens, '''Col'''ossiens, 1 '''The'''ssaloniciens,
2 '''The'''ssaloniciens, 1 '''Tim'''othée, 2 '''Tim'''othée, '''Ti'''te, '''Phil'''émon, '''Hé'''breux)
=== Apôtres de Jésus ===
'''S'''ouvent '''a'''bsent, '''J'''ean '''J'''aures '''p'''erdait '''b'''êtement '''t'''oute '''m'''onnaie le '''j'''our '''J''' de '''s'''on '''j'''eûne.
'''S'''imon, '''A'''ndré, '''J'''acques, '''J'''ean, '''P'''hilippe, '''B'''arthélémy, '''T'''homas, '''M'''atthieu, '''J'''acques, '''J'''ude, '''S'''imon, '''J'''udas.
'''Si JaJa Ma embarté Juju, J'enfile To Pierre'''
'''Si'''mon, '''Ja'''cques, '''Ja'''cques, '''Ma'''tthieu, '''An'''dré, '''Barthé'''lémy, '''Ju'''de, '''Ju'''das, '''Jean''', '''Phil'''ippe, '''Tho'''mas, '''Pierre'''.
=== '''Vertus Cardinales''' ===
'''P'''rudence, '''J'''ustice, '''F'''orce et '''T'''empérance se retient avec : " '''P'''our '''J'''ésus, '''F'''ais '''T'''out ! ", les premières lettres des mots rappellent les quatre vertus cardinales.
=== Les sept sacrements catholiques ===
''BECOROM'' :
# Baptême
# Eucharistie
# Confirmation
# Onction des malades
# Réconciliation
# Ordination
# Mariage
=== Les sept dons de l'Esprit Saint ===
''P C DS FCC (paie ces déesses fichier central des chèques)''
# Sagesse
# Discernement
# Conseil
# Force
# Connaissance
# Crainte du Seigneur
# Piété
== Navigation ==
=== Marine ===
==== Bâbord/Tribord ====
'''''Bâ'''bord c’est g'''a'''uche, t'''r'''ibord, c’est d'''r'''oite.'' (si on regarde vers la partie avant du bateau)
On peut aussi le retenir avec le mot batterie (que l’on prononce généralement « BaTri »), on a Ba à gauche (comme bâbord) et Tri à droite (comme tribord) OU plus simple avec "BaTeau".
{| class="wikitable"
| Bâ
| Tri
|-
| Bâbord
| Tribord
|-
| Gauche
| Droite
|}
Variante : la seconde lettre de b'''â'''bord est la même que la seconde lettre de g'''a'''uche et la seconde lettre de t'''r'''ibord est la même que la seconde lettre de d'''r'''oite.
Variante : Babo<s>rd ga</s>uche = Babouche.
On ne retrouve qu'un A dans bAbord et gAuche et un I dans trIbord et droIte.
==== Couleur et signalisation bâbord et tribord ====
Couleur des feux de navigation d'un bateau:
{|
| Bâbord
| '''R'''ouge
|-
| Tribord
| '''V'''ert
|}
Lu de gauche à droite cela fait '''RV''' ou le prénom '''Hervé'''.
''Un marin emporte toujours Un Tricot Vert et Deux Bas Si Rouges'' = chiffres '''impairs''', '''Tri'''bord, '''Cô'''ne, '''Vert''', et chiffres '''pairs''', '''Bâ'''bord, '''Cy'''lindre, '''Rouge'''.
Ou encore : en entrant au port, on trouve les balises COniques VERTES à TRIbord, et les balise CYlindrique ROUGES à BAbord.
En [[w:aviron|aviron]], '''T.G.V''' à savoir '''T'''ribord-'''G'''auche-'''Vert''' car le rameur est dos à l'embarcation et donc tribord se situe à sa gauche.
==== Compartimentage ====
Le compartimentage est la méthode employée pour assurer la sécurité d'un navire contre les voies d'eau. Il consiste à diviser l'espace en compartiments étanches en-dessous de la ligne de flottaison. Il résulte de ces nombreux cloisonnements une difficulté à localiser, notamment sur les gros bâtiments, tel ou tel lieu précis (cabine, soute, etc.). Afin de régler cette difficulté se pratique un référencement des locaux selon une numérotation en quatres chiffres/lettres : la tranche (de la proue vers la poupe), le pont (de la surface vers le fond à partir du pont principal 0 situé immédiatement au-dessus de la ligne de flottaison), le rang (sous-section de tranche) et le bord (local dans un rang donné).
Ex. : A216 signifie que le local se trouve dans la tranche alpha, au niveau du deuxième pont inférieur (ou faux pont), au premier rang, premier local à bâbord en partant de la ligne médiane (ou de la coursive centrale) du navire.
La mémorisation de l'ordre des bords s'effctue avec la phrase mnémotechnique suivante : "j'aime les femmes en slip", le nombre de lettres de ces mots donnant la suite 1-5-3 (impairs sur tribord) 6-2-4 (pairs sur bâbord).
==== Nœuds ====
La phrase « ''Le serpent sort du trou, tourne autour de l'arbre, et rentre dans le trou'' » permet de se rappeler de la méthode pour faire un [[w:nœud de chaise|nœud de chaise]].
=== Aviation ===
Chez les pilotes d’avions utilisant le système [[w:Precision_Approach_Path_Indicator|Vasi]] pour trouver l’altitude correcte à l’atterrissage en [[w:vol à vue|vol à vue]], des panneaux rouges et blancs à côté de la piste leur fournissent de précieuses indications qui ont abouti à cette comptine :
:« ''White over white, you're high as a kite''
:''Red over white, you're right''
:''Red over red, you're dead'' »
(Blanc sur blanc : trop haut, rouge sur blanc : correct, rouge sur rouge : trop bas).
Pour récolter toutes les informations nécessaires pour compléter leur feuille de route, les pilotes aguerris utilisent la phrase :
'''''R'''''etranchez '''v'''otre '''d'''érive, '''c'''ela '''v'''ous '''d'''onne '''c'''haque '''m'''esure '''d'''u '''c'''ap '''c'''ompas.
Route vraie - X (dérive) / Cap vrai - Déclinaison / Cap magnétique - déviation / Cap compas
Pour le décollage : CC PP VV TT (ou 3T)
Compas, Conservateur de cap (ou gyro compas), Phares, Pompe, Volets, Verrière, [[w:Transpondeur|Transpondeur]], Top, (Talons au sol). Cela dépend bien sûr du type d'avion.
Pour l’observateur au sol et face à l’avion, contrôleur, mécanicien de piste, le rouge est à droite et le vert est à gauche. Très pratique la nuit, on ne voit rien d’autre que les feux.
Moyen mnémotechnique :
Quand on se sert du vin à boire, on a le "rouge" dans la main droite et le "verre" dans la main gauche.
Ou plus facile à retenir : Comme en politique, le rouge est toujours à gauche !
== Vie quotidienne ==
La vie quotidienne regorge de moyens mnémotechniques plus ou moins utiles.
=== Droite et Gauche ===
Se souvenir de sa main habile (droitier ou gaucher)
Utiliser les initiales en majuscules (G et D) : l'arrondi est du côté correspondant (à Gauche pour G), (à droite pour D).
En anglais, pour Left et Right (Gauche et Droite). On peut faire un L avec la main gauche, avec le pouce et l'index et non avec la main droite. D'où Left=gauche et right=droite.
=== Mois courts et mois longs ===
En mettant ses poings fermés côte à côte, les bosses des [[w:phalange|phalange]]s peuvent correspondre aux mois de 31 jours du [[w:calendrier|calendrier]] et les creux entre chacune aux mois de 30 jours ou moins. Et sans compter la jonction entre les mains comme un creux.
On peut aussi le faire avec une seule main (c'est mieux pour les enfants, car ça permet de suivre le décompte des mois avec un doigt de l'autre main...) : on commence sur le premier sommet pour janvier, on s'arrête sur le dernier sommet pour juillet et, pour les mois suivants, on repart en arrière en comptant de nouveau le sommet (ou bien, on recommence au sommet initial, pour bien marquer les 2 mois de 31 jours). Attention ! Juillet-Aout ont 31 Jours mais Décembre-Janvier aussi !!
=== Heure d’été, heure d’hiver ===
En été on avance d’une heure, car "'''é'''té" et "'''a'''vance" commencent par une voyelle.
En été le soleil qui passe à travers les volets nous réveille tôt car on dort une heure en moins.
En hiver on recule d’une heure, car "'''h'''iver" et "'''r'''ecule" commencent par une consonne.
En hiver on hiberne, donc on dort une heure en plus.
Le changement d’heure se faisant en avril (!) et en octobre :
* OCTOBRE finit par RE donc on REcule
* ''AVRIL commence par AV donc on AVance''
Hélas..... cette information est affichée sur de très nombreux sites.. alors que le changement annoncé en AVRIL.. est en réalité réalisé le dernier dimanche de MARS !
Comme le changement a lieu en MARS et OCTOBRE, et qu'en général on ne peut pas (ou ne doit pas) faire reculer une aiguille sur une montre analogique :
* MARS étant plus court (4 lettres), on avance de 1 heure,
* OCTOBRE étant plus long (7 lettres), on avance de 23 heures (ou 11 heures pour les montres et horloges analogiques sans date).
Pour les anglophones : Spring Forward, Fall Back
(Spring étant le Printemps et Fall l'Automne)...
=== Retrouver de tête le nom du jour de la semaine quelle que soit la date donnée ===
{{pas clair}}
Par convention, on associe les chiffres aux lettres suivantes :
<pre>
0 = S, Z
1 = T, D
2 = N, Gn
3 = M
4 = R
5 = L, Y, ill
6 = CH, J, Ge
7 = K, Qu, Gu
8 = F, Ph, V
9 = P, B
</pre>
==== N°1 - liens entre jours et lettres : ====
Lundi est le premier jour de la semaine . Donc '''Lundi = 1''' mardi = 2 ...
==== N°2 - liens entre mois et lettres ====
===== a) correspondances pour année normale =====
:
** Janvier Février Mars... deviennent
"'''S'''a'''M''' '''M'''e '''J'''e'''T'''e'''R'''a a'''G'''e'''N'''ou'''iLL'''é '''S'''on '''M'''a'''iLL'''ot"
'''S''' = Janvier ; '''M''' = Février ; '''M''' = Mars
'''J''' = Avril ; '''T''' = Mai ; '''R''' = Juin
'''g'''= Juillet ; '''n'''= Août ; '''L'''= Septembre
'''s'''= Octobre ; '''m'''= Novembre ; '''L'''= Décembre
===== b) correspondances pour année bissextile =====
"'''G'''i'''N'''o '''M'''e '''J'''e'''T'''e'''R'''a ..."
'''G''' = Janvier ; '''N''' = Février ; etc
===== N°3 - facteur en fonction du siècle =====
====== a) Avant le 4 octobre 1582, enlever 7 au siècle (''' -7''' ) ======
Année 670 : siècle 6 '''-7''' = 0
Année 1100 : siècle 11 '''-7''' = 4
Et le résultat on le transforme :
4 devient 0 , 3 devient 1 , 5 devient 6 , 2 reste 2 et inversement.
Moyen mnémotechnique (suivant la convention) :
RuSé MaTou :: 4-0 3-1
NoNNe LouChe :: 2-2 5-6
====== b) À compter du 4 Octobre 1582 ======
soit le '''4''' - '''10''' - '''1582''' , c'est à dire le jour où le '''R'''oi '''T'''hé'''S'''ée '''T'''é'''L'''é'''PH'''o'''N'''a , on enlève 4 et non 7 , autant de multiple possible :
**Année 1800 : siècle 18 - ( '''4'''x4 ) = 2
**Année 2000 : siècle 20 - ( '''4'''x5 ) = 0
La transformation donne 0-6 1-4 2-2 3-0
{| class="wikitable"
|-
| 0 || 1 || 2 || 3
|-
| '''6''' || '''4''' || '''2'''|| '''0'''
|-
| '''CH'''è... || ...'''r'''e || '''N'''iai... || '''se'''
|-
| 0-6 || 4-1 || 2-2 || 0-3
|}
==== Calcul pour le 26 septembre 1955 ====
Il y a plusieurs opérations à réaliser en se conformant aux règles citées .
**26 Sept 1955
règle 2a ::
septembre = L = ''5''
**26+''5'' = 31
règle 3b
** 31-(7x4) = '''''3'''''
**55 - (7x4) = 55-28= 27
**27 + (27/4) = 27+6 = 33
**33 - (7x4 ) = 33-28= '''''5'''''
** '''''5 + 3 ''''' =''''' 8'''''
comme il y a 7 jours dans la semaine :
*''''' 8-7 '''''= '''1'''
le 1 correspond à '''lundi''' donc le 26 septembre 1955 était un '''lundi''', toute la journée !!!
==== Sinon pour l'année cours, passée ou à venir, ====
le moyen le plus simple et le plus rapide est de diviser l'année en trimestres . Pour chaque mois, on cherche le quantième du premier dimanche par exemple .
On fait une phrase par trimestre et pour trouver la correspondance on rajoute 7 + de 1 à 6 .
Exemple, pour 2012,
{| class="wikitable"
|-
! Janvier !! Février !! Mars
|-
| '''1''' || '''5''' || '''4'''
|-
| '''D'''e || '''<nowiki>L'</nowiki>''' || ai'''R'''
|}
Donc, le premier dimanche de Janvier 2012 est le 1er janvier . Pour aller à mon Rendez vous du 3 je rajoute 2.
** Selon ma convention déjà vue , lundi=1 mardi=2 mercredi=3 jeudi=4 vendredi=5 samedi=6 et dimanche=7 .
Alors 2 correspond à mardi donc le 3 Janvier 2012 est mardi
pour aller au 18 janvier 2012 , une semaine ayant 7 jours, même sous le règne actuel, je retire autant de semaine complète que possible . Donc 18 - (2x7 ) = 18 - 14 = 4 . Surprise, le 18 janvier 2012 sera un mercredi . Soit parce que j'ai rajouté 3 jours au dimanche, soit parce que j'ai décidé une fois pour toutes que le dimanche est le premier jour de la semaine, donc le mardi le 2ème etc ...
** dans ce cas, dimanche=1 mardi=2 mercredi=4 jeudi=5 vendredi=6 samedi=7 dimanche=8-7=1
C'est selon sa préférence intellectuelle .
Je vous laisse continuer pour février et les autres trimestres.
{| class="wikitable"
|-
! janvier !! février !! mars !! avril !! mai !! juin !! juillet !! aout !! sept !! oct !! nov !! déc
|-
| 1 || 5 || 4 || 1 || 6 || 3 || 1 || 5 || 2 || 7 || 4 || 2
|-
| D || l || r || t || ch || m || d || l || n || k || r || g
|-
| de || l' || air || tu || chô || me || dans || la || nuit || qui || rè || gne
|}
=== Valeur d'un Euro en [[w:Franc français|Francs français]] ===
Selon le même principe que pour les décimales de ''π'' :
{| border="0" cellpadding="0" cellspacing="1"
|align="center"|''Chacun''
|
|align="center"|''saura''
|
|align="center"|''enfin''
|
|align="center"|''convertir''
|
|align="center"|''notre''
|
|align="center"|''monnaie''
|-
!align="center"|6
!align="center"|,
!align="center"|5
|
!align="center"|5
|
!align="center"|9
|
!align="center"|5
|
!align="center"|7
|}
=== Morse ===
Le code morse est facilement mémorisable à l’aide des codes courts et longs remplacés par des syllabes.
Le code long (-) remplacé par une syllabe en "O".
Le code court (.) remplacé par une des autres voyelles.
Ex : A = .- = Au/tO (une syllabe en A pour le . et une syllabe en O pour le -)
La liste complète est [[w:Alphabet Morse#Tableau Mn.C3.A9motechnique|Ici]]
=== Les vins ===
==== AOC de la côte de Nuits ====
Un mnémonique permettant de se rappeler des [[wikipedia:AOC|AOC]] communales de la [[wikipedia:Côte de Nuits|Côte de Nuits]], et dans l'ordre géographique en plus, par Paul Brunet, auteur du livre ''Le vin et les vins au restaurant'' : ''Messieurs, faites gaffe, mon chat vous voit noir'' (Marsannay, Fixin, Gevrey-Chambertin, Morey-Saint-Denis, Chambolle-Musigny, Vougeot, Vosne-Romanée, Nuits-Saint-Georges).
==== Nom des bouteilles de vin ====
Ce moyen mnémotechnique permet de mémoriser les principales tailles de [[w:bouteille de vin|bouteilles]] dans l'ordre croissant de contenance :
* Car de bon matin je remarquais mal sa banalité naturelle (quart, demi, bouteille, magnum, jéroboam, réhoboam, mathusalem, salmanazar, balthazar, nabuchodonosor).
Autre phrase mnémo, plus complète :
* PICARD FIT : DE BON MATIN, JE REMARQUE SA BANALITÉ, SA MATIERE SI PAUVRE.
Pour se rappeler tous les contenants de vin ou de champagne :
* PIccolo, QUARt, FIllette, DEmi-bouteille, Bouteille, MAgnum, Jeroboam, REhoboam, Mathusalem, SAlmanazar, Balthazar, Nabuchodonosor, SAlomon, Melchisédech, Souverain, Primat.
=== <u>Dresser une table</u> ===
Pour se souvenir d'où mettre la fourchette et le couteau :
Four'''<u>ch</u>'''ette à gau'''<u>ch</u>'''e, couteau à droite.
ou encore : A, B, '''C''', '''D''' ... '''C'''outeau à '''D'''roite. E, '''F''', '''G''', H ... '''F'''ourchette à '''G'''auche
== Cinéma, Bande dessinée... ==
=== Dupond et Dupont ===
Pour différencier les deux [[w:Dupond et Dupont|dupondt]] de la bande dessinée [[w:Les Aventures de Tintin et Milou|Les Aventures de Tintin et Milou]], celui à la moustache tombante comme un D est Dupond, celui à la moustache pointue comme les barres du T est Dupont.
=== Les 7 nains ===
'''A''' '''J'''ouer '''P'''resque '''S'''eul '''T'''u '''D'''eviens '''G'''rincheux
* Atchoum, Joyeux, Prof, Simplet, Timide, Dormeur, Grincheux : les 7 nains dans Blanche Neige...
=== Les Simpson ===
Pour différencier les deux sœurs de Marge, on observe les cheveux. Patty n'a pas de raie au centre et Selma a les cheveux découpés en deux blocs par une raie ou on regarde les boucles d'oreilles qui sont différentes
Sinon, regarder les boucles d'oreilles de Patty, elles sont triangulaires (P comme Pythagore).
== Sport ==
=== Escalade ===
''Pour la prochaine longueur je reste en bas, donc je me vache au plus bas.'' <br>
:En escalade, permet de savoir sur quel point d'assurage se vacher lors du relais '''réversible''' uniquement.
== Voir aussi ==
=== Article connexe ===
* [[w:Code chiffres-sons|Code chiffres-sons]]
=== Liens externes ===
* [https://jeretiens.net -JeRetiens.net- Libre recueil ayant pour objectif de rassembler tous les trucs et astuces mnémotechniques pour retenir et apprendre plus facilement.]
* [http://www.finallyover.com/categorie-1079667.html Méthodes thématiques de mémorisation]
* [http://www.echolalie.org/wiki/index.php?ListeMnemotechnique Liste mnémotechnique]
* [http://trucsmaths.free.fr/Pi.htm#poeme Le nombre pi]
* [http://www.francaisfacile.com/exercices/exercice-francais-2/exercice-francais-75628.php francaisfacile.com]
=== Références ===
<references />
[[Catégorie:minilivres]]
6dkxb8bq7j9wusg7fw1dax6gziw9hfu
Wikilivres:Le Bistro/2011
4
32938
763173
649807
2026-04-07T16:04:19Z
JackPotte
5426
763173
wikitext
text/x-wiki
<noinclude>{{Archive du bistro}}</noinclude>
== Bonne année 2011 ==
[[File:Happy New Year 2011 banner.jpg|300px|thumb|center|'''...et bonne santé''' {{Sourire}}]]
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 1 janvier 2011 à 00:01 (CET)
:<span style=text-decoration:blink><span style="color:blue">BONNE</span> <span style="color:red">ANNÉE</span></span>. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 1 janvier 2011 à 00:20 (CET)
::Meilleurs vœux de sérieux et de dynamisme pour WIKIBOOKS, en 2011 et pour très longtemps ! --[[Utilisateur:Rical|Rical]] 1 janvier 2011 à 00:56 (CET)
:::Bonne année à tous !--[[Utilisateur:Savant-fou|Savant-fou©]] <small><sup>[[Discussion Utilisateur:Savant-fou|me parler]]</sup></small> 1 janvier 2011 à 19:18 (CET)
== Une question ==
Bonjour. Je débarque de Wikipédia, vous risquez de me retrouver bientôt pour d'autres questions mais je viens de tomber par hasard sur [[Spécial:Contributions/Anthony100000000]]. Que des articles bancals, catastrophes orthographiques, contenu mince et douteux, lorsqu'il n'est pas incohérent. sur WP on passe directement en suppression immédiate ce genre de trucs... Vous gardez exprès ? Ou on manque d'admins ? [[Utilisateur:Xic667|Xic667]] 26 janvier 2011 à 00:07 (CET)
:Il y a prescription, de plus la personne ayant l'air jeune et bien intentionnée il suffit de prendre son mal en patience. Enfin il avait le profil pour [[Wikijunior]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 26 janvier 2011 à 01:12 (CET)
== [[Wikilivres:Annonces]] ==
J'ai ajouté une annonce, mais avec votre accord je serais d'avis de la modifier pour dire que nous passons le gadget dans Common.js. De plus, je vais essayer d'ajouter un bouton pour classer par ordre alphabétique mais si vous pensez pouvoir le faire rapidement n'hésitez pas. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 10 février 2011 à 11:15 (CET)
Bon je n'ai pas pu résister j'ai ajouté le bouton dans le gadget : il reste maintenant à voter. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 11 février 2011 à 18:08 (CET)
:Que fait ce nouveau gadget précisément ?
:Il serait utile de traduire les commentaires qui sont en Polonais.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 12 février 2011 à 10:19 (CET)
::Pour l'instant je n'ai traduit que le texte qui apparait à l'utilisateur. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 février 2011 à 10:35 (CET)
== compiler toutes les pages en un livre pdf ==
bonjour
je croyais que c'était simple de compiler à tout moment en un ou qqs clics et obtenir la version pdf.
J'ai essayé avec les liens sur la gauche, ça m'a donné que le sommaire... comment faire pour que je sorte au plus vite toutes les pages d'un seul coup ?
merci à vous
:Il faut créer une page appelant toutes les autres, comme dans [{{fullurl:Le_langage_HTML/Version_imprimable|action=edit}} Le langage HTML/Version imprimable]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 10 février 2011 à 18:37 (CET)
::Un autre exemple avec page de couverture, date et nombre de pages sur la couverture, pages de chapitre plus distinctes, bibliographie, [{{fullurl:Une_histoire_des_transmutations_biologiques/Version_imprimable|action=edit}} Une histoire des transmutations biologiques/Version imprimable] --[[Utilisateur:Rical|Rical]] 10 février 2011 à 23:57 (CET)
:::Le moyen de JackPotte n'est pas le plus approprié pour obtenir un PDF rapidement. Le mieux est d'utiliser [[Special:Book]] tel que décrit sur [[Aide:Compilations]], en ajoutant directement à la compilation [[:Catégorie:Une histoire des transmutations biologiques|tout le contenu de la catégorie du livre]] (dans le cas ici des transmutations biologiques). [[Utilisateur:Savant-fou|Savant-fou©]] <small><sup>[[Discussion Utilisateur:Savant-fou|me parler]]</sup></small> 11 février 2011 à 16:28 (CET)
::::C'est exact, les compilations sont adaptées, j'ai juste l'impression que tu n'ajoutes pas toutes les pages à ta compilation (il faut vraiment aller sur chaque page et cliquer sur « Ajouter à la compilation » dans la menu à gauche). Pour ce qui est des versions imprimables dont JackPotte et Rical citent des exemples, elles sont obsolètes : les compilations font bien mieux, elles permettent d'obtenir un livre imprimé, un PDF ou une version ODT. [[Utilisateur:Sub|Sub]] 11 février 2011 à 20:01 (CET)
:::::Les compilations peuvent être créées manuellement : la syntaxe est assez simple (voir [[Wikilivres:Compilations/Méthodes de propulsion spatiale]] ou [[Wikilivres:Compilations/Programmation C sharp]]), cela évite la visite de chaque page si on a la liste des pages du livre (le sommaire par ex).
:::::Cependant la version imprimable reste utile pour contrôler la présentation du livre imprimé. Elle peut ensuite faire partie d'une compilation ne contenant que cette page.
:::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 12 février 2011 à 10:18 (CET)
== Complétion de recherche cassée ? ==
J'ai l'impression que la complétion des recherches ne marche plus ici. On a eu le même problème sur le Wiktionnaire où j'ai constaté que le problème venait de la partie de code correspondant à la copie de code de [[wikt:de:MediaWiki:If-search.js|de.wiktionary.org/wiki/MediaWiki:If-search.js]] (dans [[Mediawiki:Common.js]]). Si un admin codeur pouvait vérifier (en commentant cette section par exemple), pour voir si le problème vient bien de là... [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 17 février 2011 à 14:07 (CET)
:Aucun problème sous monobook, cela concerne vector : je vais le commenter en attendant (en plus il faisait ramer mon PC). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 17 février 2011 à 18:08 (CET)
:: Merci ça remarche (sous Vector) :) [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 18 février 2011 à 10:55 (CET)
== Wikilivres de biographies ==
Bonjour, je suis tombé sur quelques pages de biographies (j'en ai proposé [[Wikilivres:Pages à supprimer|deux à la suppression]]) avant de m'apercevoir que ce n'était que deux pages parmi un ensemble conséquent de biographies d'artistes, le tout réuni dans des pages [[Art et écologie]], [[Art contemporain]], [[Art vidéo]], [[Arts sonores]], [[Art contemporain en Languedoc-Roussillon]] (j'en oublie probablement).
Le tout semble être un projet rédigé par des étudiants sous l'aile de {{u|Stéphan BARRON}}.
Le problème est que je ne vois pas ce que ça fait sur Wikilivre. Je comprend l'intérêt pédagogique pour des étudiants de rédiger des biographies, mais [[Wikilivres:Présentation|en quoi un annuaire d'artistes est-il pédagogique]] ? Ce genre de travaux aurait certainement plus sa place dans un wiki personnel qu'ici.
Je suis d'avis de supprimer ces pages, en attendant cependant que ces travaux soient sauvegardés et déplacés dans un autre wiki par les intéressés. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 23 février 2011 à 14:56 (CET)
:[[Wikilivres:Le_Bistro/Archives/2010_-_deuxième_trimestre#Lassitude|Quentinv57 avait commencé]], mais il faut savoir avant tout que le contenu de [[:Catégorie:Arts]] rentre dans [[Wikilivres:Le_guide_de_l'administrateur#Contributions_ind.C3.A9sirables_et_vandalismes|nos critères d'admissibilité actuels]]. Personnellement ce sont les seules pages que je laisse non patrouillées en attendant que tout le monde trouve l'énergie de décider de leur sort. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 23 février 2011 à 19:24 (CET)
:: Certes, l'art a tout à fait sa place ici, comme en témoigne le [[Photographie|grand livre de photographie]], du moment que c'est présenté de manière [[Wikilivres:Présentation|pédagogique]]. Or je peine à voir ce que les pages dont je parle ont de pédagogique, et j'estime donc qu'elles ne remplissent pas les critères d'admissibilité. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 23 février 2011 à 20:57 (CET)
:::Que faire, importer brutalement {{WP|Wikipédia:Critères d'admissibilité des articles}} et supprimer ces dizaines de pages par bot, ou bien voter pour une page équivalente ici et trancher au cas par cas ? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 23 février 2011 à 21:37 (CET)
:::: Je pensais plutôt à la version anglophone : [[:en:Wikibooks:What is Wikibooks]], mais la plupart de ces points sont déjà présents dans [[Wikilivres:Présentation]]. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 24 février 2011 à 11:40 (CET)
:::: « importer brutalement {{WP|Wikipédia:Critères d'admissibilité des articles}} » : surtout pas. Déjà, il faudrait écrire notre page [[Wikilivres:Principes fondateurs]]. Nos principes fondateurs devraient être suffisamment éloquents et précis pour nous guider dans les choix en toute circonstances (un peu comme un constitution). En fait, je crois que nous devrions nous restreindre à « toutes les règles doivent être exprimées sur une seule page », ceci afin que tous les contributeurs puissent en prendre connaissance. Si on commence à créer pages de pages de règles, on va se retrouver comme sur Wikipédia, où il faut un Bac + 12 en droit pour savoir ce qu'on a le droit de faire. Le biographies en question, le cas me parait difficile à trancher, pour l'instant. [[Utilisateur:Sub|Sub]] 25 février 2011 à 00:14 (CET)
== Logo ==
[[Fichier:Wikibooks-logo-fr-sans.svg|thumb|120px]]
Je me demande depuis que je suis revenu ici : y'a-t-il une raison qui explique pourquoi le logo n'a pas le titre français comme dans le reste du site (et comme l'ont fait d'autres langues) ? [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 24 février 2011 à 11:51 (CET)
:Il faut le dire à Bugzilla pour bien faire, et c'est assez long. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 24 février 2011 à 18:54 (CET)
:Oui, il y a une raison. Cela vient du faites que nous ne nous sommes jamais mis d'accord sur le nom ''Wikilivres'' : d'une part, cela ne traduit pas bien la notion de texte pédagogique (il faut savoir que wikibooks vient de ''wiki'' et de ''textbooks'' qui veut dire « manuel » comme dans « manuel scolaire ») et d'autres part, il y a risque de confusion avec un autre projet sur la Toile qui s'appelle Wikilivres. C'est pourquoi, nous avons choisis de garder Wikibooks mentionné un peu partout et de retrouver notre projet sur la Toile, sans tomber sur... Wikilivres. On devrait pouvoir retrouver tout ça dans les archives du bistro. [[Utilisateur:Sub|Sub]] 25 février 2011 à 00:03 (CET)
:: Merci pour ces précisions très intéressante (peut-être que cette explication aurait sa place dans la page de présentation du projet). Maintenant, comme renommer le logo en ''Wikilivres'' n'est pas recommandé, peut-être serait-il judicieux tout de même de rajouter un slogan ? Quelque chose comme « des livres (libres) pour apprendre », afin de ne pas faire croire que c'est simplement un « wiki de livres ». La traduction du slogan anglophone ne me parait pas convaincante (« Des livres libres pour un monde libre »). [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 25 février 2011 à 11:01 (CET)
:::Autre raison qui m'est revenue à l'esprit, le nom « Wikibooks » est déposé par la fondation Wikimedia et nous protège du plagiat ou de l'usurpation, ce n'est pas le cas pour « Wikilivres ». Quant au slogan, nous avons aussi vainement cherché sans dégager un slogan consensuel. Toutefois, nous utilisons pour les [[Wikilivres:Promotion|dépliants]] « Des livres libres, pour tous » mais convenons que ce n'est pas vraiment idéal... [[Utilisateur:Sub|Sub]] 25 février 2011 à 14:05 (CET)
::::Comme c'est une question qui revient souvent, je l'ai traitée, parmi d'autres dans notre guide [[Wikilivres]] : [[Wikilivres/La communauté Wikilivres#« Wikilivres » et « Wikibooks », la non-francisation du nom et du logo du projet]]
== Principes fondateurs ==
Je viens de rédiger le noyau de nos [[Wikilivres:Principes fondateurs]]. À critiquer et à valider par chacun d'entre vous. Certains points vous peut-être vous paraître bizarre (genre le point 9), mais pour chacun d'eux j'ai deux/trois exemples de problèmes qu'on a pu avoir ces dernières années. Les points sont numérotés pour faciliter nos discussions. [[Utilisateur:Sub|Sub]] 25 février 2011 à 01:20 (CET)
:Premières remarques :
:* Pour le point 4, on peut aussi dire que ces rôles ajoutent des tâches supplémentaires plutôt que des pouvoirs.
:* Il faudrait donner un résumé de tous ces points (un seul mot par point si possible, ou leur donner un titre) afin d'avoir l'essentiel en un seul coup d'œil.
:* Je n'ai pas vu de point évoquant le fait qu'un livre n'appartient pas exclusivement à un auteur ou un groupe d'auteurs (étudiants par exemples), et que chacun est libre de reprendre un livre, ajouter du contenu, corriger les fautes, ... en évitant le remaniement massif sans discussion préalable. À ajouter comme précision dans le point 1 (projet collaboratif).
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 février 2011 à 00:38 (CET)
::Dans le cadre de la publication d'un vocabulaire positif, je remplacerais le cauchemardesque ''Wikibooks veille à ne pas faire de mauvaise concurrence aux autres projets de rédaction de contenu Libre'', par ''Wikilivres est complémentaire aux autres projets Wikimédia, et par conséquent il convient d'en limiter les redondances''. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 12:18 (CET)
:::Je vous invite à faire les corrections souhaitées vous-même, vue la nature du projet, il est tout à fait souhaitable que nous ayons rédigé ce document fondamental à plusieurs. Remarque à JackPotte : par ''autres projets de rédaction de contenu Libre'', je ne voulais pas parler des projets Wikimedia (bien qu'on puisse les inclure), mais des autres projets similaires en Wikibooks en général comme FlossManuals, Framabook, Sésamath, etc. [[Utilisateur:Sub|Sub]] 27 février 2011 à 13:32 (CET)
::::Je m'en doutais mais persiste pour ne pas imposer à tous nos éditeurs la lecture de tous ces sites (ou alors dans la mesure du possible). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 13:44 (CET)
:::::Loin de moi l'idée d'imposer la lecture de ces sites, ni même leur connaissance. Simplement, je ne veux pas que quelqu'un aille piller sur un de ces sites pour tout importer ici et laisser en plan ou pire faire un fork alors que le projet aurait très bien pu suivre son cours ailleurs sans ennuyer le lecteur. Je suis d'accord que ma formulation originale n'est pas heureuse, mais il s'agit simplement de dire que Wikibooks doit vivre en bonne intelligence et considérer ces projets comme partenaires (qui ont une autre approche mais le même objectif), non comme des concurrents. Rien de plus. [[Utilisateur:Sub|Sub]] 27 février 2011 à 13:54 (CET)
::::::Dans ce cas je suggère la formule ''Préférer placer les autres projets de rédaction de contenu Libre en référence, que les copier''. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 14:53 (CET)
=== Minimum légal pour décider ? ===
Un point n'est pas clair, sur la possibilité de décision pour les nouveaux. Les principes énoncent qu'il faut un minimum d'expérience pour pouvoir ''la ramener'', ce serait peut-être pas mal de fixer les choses non ? Par exemple, exiger un compte créé depuis plus d'un mois + un historique de contribution d'au moins 50 entrées, ça irait ? [[Utilisateur:Sub|Sub]] 27 février 2011 à 14:00 (CET)
:Ça semble pas mal en effet. Il me semble bon de préciser 50 contribs dans ''main''.--[[Utilisateur:Savant-fou|Savant-fou©]] <small><sup>[[Discussion Utilisateur:Savant-fou|me parler]]</sup></small> 27 février 2011 à 14:11 (CET)
::{{fait}} [[Utilisateur:Sub|Sub]] 30 avril 2011 à 18:20 (CEST)
== Portail ==
Bonjour à tous, j'ai reçu ce message de la part de DavidL ;
Bonjour,
Avant de créer les portails, et faire de nombreuses modifications que l'on risque de devoir annuler, il faudrait d'abord [[Wikilivres:Le_Bistro|en discuter]]. Sachant qu'ici ce n'est pas wikipédia, est-ce que portail est un nom approprié ? De plus cela casse [[Wikilivres:Conventions sur les titres|la convention sur les titres]] : utiliser le slash dans les titres et pas le caractère deux-points réservé aux espaces de nom.
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 février 2011 à 01:56 (CET)
Je veux bien croire que wikibooks n'est pas Wikipédia, mais pourtant d'autres projets utilisent le même système ;
*[[:wikt:Catégorie:Portails]] sur le Wiktionnaire
*[[:n:Catégorie:Portail]] sur Wikinews sous le nom de ; Page
*[[:s:Catégorie:Portails]] sur Wikisource
*[[:v:Catégorie:Faculté]] et sur Wikiversité, il y a deux espace de noms ; Faculté et Département
Et ici même il y a déjà ceci ; [[Portail:Montagne]]
Merci beaucoup pour vos avis. Bien Cordialement. [[Utilisateur:FrankyLeRoutier|FrankyLeRoutier]] % [[Discussion utilisateur:FrankyLeRoutier|Service après-vente]] 27 février 2011 à 02:36 (CET)
: Un portail, c'est juste quelques pages pour organiser un domaine, un aspect, des taches, c'est très bien. Et ils apparaissent peu à peu parce que le nombre de livres augmente.
: [[Wikijunior]] est un portail sans le dire. Les catégories sont des "mines à portails". Certaines pages utilisateurs sont déjà des mini-portails, il suffit d'en prendre conscience et de les mettre en commun.
: Après quelques discutions, évidement, mais sans rigidité. --[[Utilisateur:Rical|Rical]] 27 février 2011 à 12:06 (CET)
::Voyez-vous s'il n'y a pas "portail" dans la liste avancée de [[Spécial:Recherche]] (ni même "projet"), c'est que nous n'en avons pas eu l'utilité. Personnellement je trouve que les portails et les projets sont redondant des catégories (ils permettent à la rigueur de classer les mêmes pages par thèmes au lieu de l'ordre alphabétique, en attendant des sous-catégories ou un CSS dynamique). Bref, pour plus de simplicité et moins de redondance, je propose de déplacer [[Portail:Montagne]] (et [[Projet:Montagne]]) entre [[w:Portail:Montagne]] (et [[w:Projet:Montagne]]) et [[:Catégorie:Montagne]] (donc compatible avec encore plus d'interwikis). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 12:18 (CET)
::PS : [{{fullurl:Portail:Informatique|action=historysubmit&diff=314882&oldid=314881}} révertons ce système], car
::#La charte impose déjà les sous-pages, expliquées en détails.
::#Si toutes ces sous-pages sont listables par un lien (gadget sous-pages) et [[special:index]], les portails et projets nécessiteraient de cocher une case de [[special:search]] qui n'existe pas. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 12:22 (CET)
:::Rebonjour, la version anglophone de wikibooks utilise un espace sujet ; [[:en:Subject:Books by subject]] et il serait possible à mon humble avis de rajouter dans les options de recherches l'espace Portail. [[Utilisateur:FrankyLeRoutier|FrankyLeRoutier]] % [[Discussion utilisateur:FrankyLeRoutier|Service après-vente]] 27 février 2011 à 13:23 (CET)
::::D'accord avec JackPotte. D'après mon expérience : les portails, étagères, projets et autres ne sont pas souhaitables : ils sont redondants avec les catégories, contribuent à l'accumulation de ''non-contenu'' et consomme du temps et de l'énergie. Le fait est que ces portails ne s(er)ont pas maintenus et donnent une mauvaise image du projet. Je pense qu'il faut rester sur le système des catégories et ne pas hésiter à enrichir les pages catégorie (rappelons que ce sont des pages modifiables comme les autres). À la rigueur, trouver quelque-chose de minimaliste comme les collections que j'ai un peu élaborer avec Savant-fou notamment. Voir par exemple, {{m|Collection logiciels libres}} ou {{m|Collection débuter sur internet}} : du léger, du simple, pas de fioritures et ça me semble suffisamment minimaliste pour être maintenable. Quant aux anglophones, ils ont bien plus de moyens que nous (en terme de contributeurs actifs), par ailleurs, eux-même ne sont pas satisfait de ce système (cf leurs discussions sur leur bistro). [[Utilisateur:Sub|Sub]] 27 février 2011 à 13:26 (CET)
:::::Idem. J'ai créé le portail Montagne lors de mon arrivée sur Wikibooks en 2008, sans trop connaitre les us et coutumes locaux, en m'inspirant de ce qui est fait sur Wikipédia. Les collections semblent être une alternative profitable, car elles ne demandent pas trop de maintenance, et évitent de créer de « non-contenu ». Je suis donc pour virer mon portail et en faire une collection. Je m'occupe de ça rapidement si tout le monde est d'accord.--[[Utilisateur:Savant-fou|Savant-fou©]] <small><sup>[[Discussion Utilisateur:Savant-fou|me parler]]</sup></small> 27 février 2011 à 13:54 (CET)
:::::: Ouais, la dernière actu de ce portail date de deux ans :-(. En me relisant j'ai eu une idée. Si effectivement on décide de rester sur le système des catégories, on pourrait peut-être encouragé la rédaction des pages de catégories pour introduire un peu le thème, non ? [[Utilisateur:Sub|Sub]] 27 février 2011 à 14:18 (CET)
:::::::Je pense que le lien vers la catégorie Wikipédia introduit déjà le thème. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 14:54 (CET)
Bon comme nous sommes d'accords, je vais prochainement pouvoir tester mon robot révocateur... [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 14 mars 2011 à 02:00 (CET)
:{{fait}} (le bot ayant fait la fine bouche j'en ai fait la moitié en personne). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 14 mars 2011 à 20:15 (CET)
== [[Modèle:BookCat]] ==
A le demande de Sub je viens d'importer ce modèle : il pourrait remplacer les différentes catégories des sous-pages de nos livres. Comment l'appeler : {{m|cat}}, {{m|cat auto}}, {{m|autocat}}, {{m|cat livre}}, {{m|catégorie}}, tout cela... [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 19:53 (CET)
== Salon Jabber ==
Sub propose de créer un salon Jabber Wikilivres, un peu comme wikipedia-fr@muc.jappix.com. Pour ma part je propose d'en parler à tous les Wikibooks pour en faire un lieu de synchronisation interwiki. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 février 2011 à 19:59 (CET)
== Approche formelle d'un cours et interprétations de l'élève ==
Bonjour,
Je voulais vous signaler la petite discussion que j'ai initiée ici : [[Discussion:Manuel de Sciences Physiques φχ -- classe de cinquième/Circuit électrique/Circuit électrique/cours]]. C'est appuyé sur un cas particulier, mais ça vaut probablement pour d'autres cours.
Ce qui ne me convient pas trop dans cette présentation d'un cours, je sais bien que c'est un modèle très courant, et qui n'est pas spécifique à Wikibooks, mais comme je le relève ici comme j'aurais tendance à le relever venant d'un manuel scolaire ou d'un cours classique. Et en fin de compte, le modèle de cours de Wikibooks comme de la Wikiversité me semble très traditionnel, dans le sens formaliste, où on ne se soucie pas, ou même on évite que l'élève rattache la notion étudiée à ses connaissances informelles ou à son expérience sensible. [[Utilisateur:Astirmays|Astirmays]] 5 mars 2011 à 09:54 (CET)
:Il s'agit d'un livre ouvert. Le court est loin d'être parfait. C'est l'avantage de wikibooks : pouvoir modifier les livres pour les améliorer.
:Puisque tu connais bien le sujet du cours, tu pourras certainement remanier [[Manuel de Sciences Physiques φχ -- classe de cinquième|le livre]] qui semble inachevé et abandonné.
:Ce qu'il faut avant tout c'est proposer un sommaire clair qui définisse les titres des pages (voir [[Wikilivres:Conventions sur les titres]]).
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 5 mars 2011 à 10:32 (CET)
== bac à sable ==
peut-on savoir qui écrit entre parenthèses "bac à sable" à la fin du titre indiquant un nouveau message? {{non signé|88.187.164.126}}
:[[Spécial:Abusefilter|C'est moi]] et en l'occurrence c'était justifié : avez-vous lu [[Aide:Accueil|la charte]] du site avant de poster sur votre page de discussion ? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 11 mars 2011 à 22:18 (CET)
== Supprimer [[:Catégorie:Ingrédients]] ==
Après en avoir lu plusieurs, je constate que ces pages sont trop redondantes de celles du Wiktionnaire et de Wikipédia.
La solution serait, soit de les transformer en liste de recettes contenant cet ingrédient (redirection vers une catégorie), soit en redirection (douce ou dure) vers le Wiktionnaire. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 mars 2011 à 16:35 (CET)
:Toutes ces informations sont utiles pour le [[Livre de cuisine]], par contre une page par ingrédient est beaucoup trop. Un bot pourrait-il passer pour prendre toute ces pages pour les fusionner en une seule, découpée en sections (une page → une section avec le même titre) ? Le bot pourrait mettre tout ça sur la page [[Livre de cuisine/Ingrédients]] ou sur une page temporaire, je me chargerai d'organiser la page résultat. Une fois ceci fait, pas de raison de conserver la catégorie Ingrédients. [[Utilisateur:Sub|Sub]] 13 mars 2011 à 19:13 (CET)
::Techniquement je peux le faire en laissant des redirections sur chaque page d'ingrédient. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 14 mars 2011 à 01:59 (CET)
:::Et si en plus, ça pouvait fusionner les historiques, parfait. [[Utilisateur:Sub|Sub]] 18 mars 2011 à 20:22 (CET)
::::A la réflexion je ne comprends toujours pas les avantages de rediriger nos ingrédients vers une liste de ceux-ci, je propose plutôt que quand on clique dessus, on arrive sur Wikipédia (et pour le Wiktionnaire il y a déjà un gadget qui le fait sur double-clic). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 9 avril 2011 à 22:03 (CEST)
:::::Ouais, je viens de jeter un coup d'œil aux pages concernées, la plupart sont pratiquement vides et n'apportent rien d'intéressant. Je me joins à ton opinion, OK pour la suppression.
:{{bot en cours|JackBot|JackPotte}} 10 avril 2011 à 14:15 (CEST)
:{{bot fait|JackBot|JackPotte}} 20 avril 2011 à 22:13 (CEST)
== Nouveau conseil d'administration pour Wikimédia France ==
Bonjour,
Samedi dernier Wikimédia France a tenu son assemblée générale annuelle. Nous avons pu faire le point sur les actions de l'année, celles que nous voulons mettre en place dans les mois qui viennent etc. Le rapport annuel 2010 sera très prochainement en ligne sur notre [http://www.wikimedia.fr site].
Nous avons également élu le nouveau conseil d'administration :
* Adrienne Alix, présidente
* Rémi Mathis, vice-président
* Willie Robert, secrétaire
* Thierry Coudray, trésorier
* Sébastien Beyou, trésorier-adjoint
* Florence Devouard
* Christophe Henner
* Pierre-Yves Mevel
Comme vous le savez, les associations locales ([[w:Wikipédia:Wikimédia France|Wikimédia France]], [[w:Wikipédia:Wikimedia CH|Wikimédia CH]] etc) ont pour objectif de soutenir les projets Wikimédia. N'hésitez donc pas à faire appel à nous si vous avez des projets, des besoins, etc. A bientôt ! [[Utilisateur:Serein|Serein]] 14 mars 2011 à 22:00 (CET)
== {{m|étiquette progression}} ==
Je vous propose ce nouveau modèle, à poser sur un livre. Il permet au lecteur de savoir s'il peut se servir d'un livre comme support d'apprentissage ou si le contenu n'est pas mature. J'ai essayé de donner une touche moderne aux icônes. Utiliser ce modèle me permet plus pertinent que la catégorisation à la main. Pensez-vous que le découpage est pertinent ? [[Utilisateur:Sub|Sub]] 18 mars 2011 à 20:26 (CET)
:[[File:Comedyfilm.png|50px]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 18 mars 2011 à 21:34 (CET)
::Bon découpage et bonne idée. -- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 18 mars 2011 à 23:52 (CET)
::: J'aime bien, par contre le smiley triste pour l'ébauche me parait un peu trop négatif. Ce serait bien de trouver une autre illustration plus avenante, si possible {{sourire}}. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 21 mars 2011 à 17:36 (CET)
:::: Pour éviter des décalages verticaux différents pour les divers cas, j'ai masqué des retours lignes par des commentaires entre les cas, et j'ai reporté plusieurs sauts de ligne à la fin. Mais je suis pas sur du nombre qu'il en faut sur les pages finales. On pourra corriger après. --[[Utilisateur:Rical|Rical]] 21 mars 2011 à 21:42 (CET)
=== {{m|étiquette niveau}} ===
Dans le même ordre d'idée, je vous propose ce modèle pour classer selon le niveau. Remarquez que le découpage selon le niveau n'est pas strict, cela afin de prendre en considération les différentes France / Québec où le changement de niveau ne se fait pas au même moment. Le modèle ajout le catégorie de niveau si possible, et la catégorie Wikijunior si pertinent. À voir si c'est une bonne pratique, nous pourrons toujours revenir dessus si ça provoque un bourrage dans la catégorie. [[Utilisateur:Sub|Sub]] 30 avril 2011 à 18:09 (CEST)
== Flux RSS nouveaux livres ==
Existe-t-il un flux RSS pour les [[:Catégorie:Nouveau livre|nouveaux livres]] ?
[[Utilisateur:PAC2|PAC2]] 8 avril 2011 à 15:58 (CEST)
:[[:en:Wikibooks:Study_help_desk/Archive_(March_2005-June_2006)#RSS_Feed_for_an_entire_book|Normalement non]], mais on pourrait le faire comme Wikinews (même si actuellement il renvoie ''403: User account expired''). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 8 avril 2011 à 16:39 (CEST)
::JackPotte répond à côté. PAC2, il n'y a pas de flux RSS pour les nouveaux livres : dommage. À une époque, je les annonçais tous sur mon blog mais le faible nombre de lecteurs m'a fait abandonner l'idée. Si tu souhaites le faire, je peux te donner un accès au blog pour que tu puisses produire un tel flux (il pourra ainsi être diffusé sur planet wikimedia). JackPotte, tu fais référence à une discussion sur l'idée d'avoir des flux pour chaque livre : ce n'est pas ce que demande PAC2. Le lien que tu montres date de quelques années et nous avons ça (ça reste une bidouille) : {{m|flux du livre}}. [[Utilisateur:Sub|Sub]] 8 avril 2011 à 22:30 (CEST)
::: Merci pour vos réponses. Je ne souhaite pas spécialement m'y abonner mais je pense que ça pourrait être un moyen de faire une meilleure publicité pour les nouveaux livres. [[Utilisateur:PAC2|PAC2]] 9 avril 2011 à 20:12 (CEST)
== Aide pour les Wikipédiens ==
Bonjour, je me demandais s'il existait une page d'aide pour les Wikipédiens expliquant les grosses différences de fonctionnement, sans rentrer dans les détails de syntaxe wiki ou de licence à peu près communs aux différents projets. Pour info, j'ai fait [[:n:Aide:Wikinews pour les Wikipédiens]] et je pense que ça serait intéressant de faire la même chose sur WikiBooks et les autres projets. Si ça vous intéresse, je pense mettre un mot sur le bistro de Wikipédia d'ici une ou deux semaines pour faire la pub de ces pages d'aide. [[Utilisateur:Moyg|Moyg]] 14 avril 2011 à 00:08 (CEST)
:Bonjour,
:Moi-même Wikipédien à la base, je trouve effectivement que ça peut être une bonne idée. Sur l'aide de Wikilivre, il existe une page ''[[Wikilivres/Wikilivres et Wikipédia]]'', peut-être que ce serait le bon endroit... [[Utilisateur:Binabik155|Binabik]] ([[Discussion Utilisateur:Binabik155|d]]) 14 avril 2011 à 16:11 (CEST)
::Oui, c'est exactement la page prévue pour les wikipédiens. [[Utilisateur:Sub|Sub]] 15 avril 2011 à 01:01 (CEST)
:::OK, j'ai rajouté quelques petites informations/astuces sur la page alors. N'hésitez pas à compléter. [[Utilisateur:Binabik155|Binabik]] ([[Discussion Utilisateur:Binabik155|d]]) 19 avril 2011 à 14:05 (CEST)
::::Merci ! [[Utilisateur:Sub|Sub]] 19 avril 2011 à 20:01 (CEST)
== wikilivre mac os X ==
J'avais commencé un wikilivre sur [[Mac_os_X|Mac OS X]]. Je n'utilise plus Mac OS depuis mon passage sur Ubuntu et je ne prend donc plus le temps de développer ce livre. Comme le livre est encore à l'état de brouillon et qu'il n'est plus développé, il est aujourd'hui menacé de suppression. Je pense qu'il n'existe pas de manuel libre sur Mac OS X et c'est dommage. Y a-t-il des personnes intéressées pour reprendre le projet à leur compte ? [[Utilisateur:PAC2|PAC2]] 23 avril 2011 à 10:34 (CEST)
:Oui, on en parle déjà sur [[Wikilivres:Pages_à_supprimer#Mac_os_X]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 23 avril 2011 à 10:35 (CEST)
== Remaniement du [[livre de cuisine]] ==
Comme indiqué il y a quelques temps, je m'attaque au livre de cuisine. Comme prévu, j'ai commencé par extraire de ce livre tout ce qui n'était pas ''recette'' à proprement parler dans un livre à part entière : [[Apprendre à cuisiner]]. J'ai pris tout ce qu'il y avait à prendre dans le livre de cuisine, il faut donc maintenant le remanier et ajouter du contenu pour en faire quelque chose de plus pédagogique, qui permet d'apprendre les choses au fur et à mesure.
J'ai également créé le livre [[Recettes de cuisine]], dont le sommaire est basé sur une liste dynamique de catégories. Comme vous pouvez le voir, il y énormément de tri à faire, notamment parce que les noms de catégories ne sont pas bon. Cherchez un peu, vous pouvez tomber sur des dinosaures. Il y a donc un travail colossal et assez peu automatisable à faire :
* Renommer les pages en sous-pages du livre [[Recettes de cuisine]]
* Renommer les catégories avec des noms corrects (Recettes à base de poulet, plutôt que simplement ''Poulet'')
* Classer les recettes non-classées
Comme c'est un travail assez monstrueux (plusieurs milliers de recettes), j'ai toujours tendance à décomposer les problèmes en sous-problèmes, plus simples. C'est pourquoi j'avais prévu de déplacer tout ce qui concerne les boissons dans un livre à part entière (''Recettes de boissons''). Ceci en partant du principe qu'il s'agit d'un domaine de connaissance à part (les barmans ne font pas de cuisine), qu'il vaut mieux quelques livres avec un périmètre bien définis qu'une encyclopédie fourre-tout (le livre de cuisine tel qu'il était avant que je ne m'y attaque) et que de toute façon, si quelqu'un veut vraiment avoir toute les recettes, il lui suffira d'imprimer deux livres plutôt qu'un seul...
Je voulais donc savoir si, dans la communauté, les gens étaient favorables à la création d'un livre de ''Recettes de boissons'' (pour le titre, on pourrait trouver mieux) comme moi, où si ils pensent qu'au contraire, il faut garder un livre unique, comme le pense {{u|JackPotte}} (qui est invité à exprimer son point du vue en détail).
Merci pour vos avis.
[[Utilisateur:Sub|Sub]] 24 avril 2011 à 13:31 (CEST)
:Deux livres me semble OK : quand on cuisine, on ne veut pas nécessairement faire des cocktails. Comme il s'agit d'un wiki, on peut très bien ajouter (quand c'est adéquat) des liens vers le livre de boissons dans une recette (rubrique "Boissons suggérées" par exemple).
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 24 avril 2011 à 15:15 (CEST)
::Aucun problème non plus avec cette idée.
::Par contre, tu es sûr que le renommage des sous-pages du livre [[Recettes de cuisine]] ne peut pas être automatisé un minimum (par exemple en corrigeant tous les articles de [[:Catégorie:Recettes|Catégorie:Recettes]]) ? [[Utilisateur:Binabik155|Binabik]] ([[Discussion Utilisateur:Binabik155|d]]) 26 avril 2011 à 15:25 (CEST)
La suite sur [[Discussion:Recettes de cuisine]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 2 mai 2011 à 23:11 (CEST)
== Bouton de signature ==
Pourquoi le bouton de signature met automatiquement deux traits (--) avant le signature ?<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 28 avril 2011 à 14:23 (CEST)
:Pour séparateur quand la signature est sur la même ligne que le texte...-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 28 avril 2011 à 18:52 (CEST)
::Mais on peut modifier la fonction pour n'ajouter le double-tiret que si la signature n'est pas en début de ligne.-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 28 avril 2011 à 19:05 (CEST)
:::OK.<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 29 avril 2011 à 14:26 (CEST)
== Problème heure. ==
[[Spécial:Modifications récentes]] J'ai eu un problème d'heure. J'ai 2 heures d'avance. Y a t-il une façon d'y remédier. <br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 30 avril 2011 à 14:32 (CEST)
:Regarde dans [[Spécial:Préférences#preftab-2|tes préférences pour date et heure]].
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 30 avril 2011 à 16:27 (CEST)
::Ok. Merci pour le coup de main.<br />
::[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 30 avril 2011 à 18:31 (CEST)
== Problème de connexion ==
Bonjour,
Depuis quelques jours, quand j'enregistre des modifications, quelques fois, je ne suis plus connecté à Wikibooks ou à Wikiversité. Pourtant, j'ai bien la connexion Internet activée et je reçoit un bon voire très bon signal. Cela n'arrive qu'avec les sites de la Wikimedia Foundation. Y-a-t-il un problème particulier avec les serveurs? Sinon, est-ce-que cela peut venir de mon système ou de mon navigateur. Est-ce-que cela peut venir de mon fournisseur Internet. J'avais plusieurs onglets d'ouverts en même temps. 1 sur Wikibooks, 2 sur Wikiversité, 1 en recherche Google, 1 sur le Blender clan, et seul les 2 sur la Wikiversité et celui sur Wikibooks on été touchés. Pouvez-vous m'aider. <br />
Problèmement,<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 1 mai 2011 à 08:48 (CEST)<br />
P.S: j'avais copié le texte si l'erreur se reproduisait encore.
:Je suppose qu'il faut cocher les deux cases "me connecter" dans [[spécial:Connexion]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 1 mai 2011 à 12:03 (CEST)
::Je ne parle pas de connexion à Wikibooks, mais de connexion au site. L'ordinateur ne recevait plus de réponse du site.<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 1 mai 2011 à 15:50 (CEST)
:::Si c'est le fameux message "Wikimedia error", je l'ai au moins une fois par jour aussi. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 1 mai 2011 à 16:18 (CEST)
== besoin d'aide ==
je n'ai plus plus mon code et mon nom d'utilisateur quelqu'un peut me dire comment le retrouver ? je dois ajouter des photos dans 2 livres
Merci
sharia34@aol.com
:Dans les messages de ta boîte mail.
:Sinon un autre compte peut être créé.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 2 mai 2011 à 18:40 (CEST)
== [[Spécial:Pages demandées]] ==
Bonjour, juste une petite note pour vous prévenir que le nombre de pages demandées déborde. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 2 mai 2011 à 19:59 (CEST)
:"La dernière actualisation date du 22 janvier 2011 à 14:25."
:Les deux premières de la liste sont OK maintenant. Il faudrait demander une mise à jour plus fréquente des données (2 fois par jour minimum) sinon la page est inutile.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 3 mai 2011 à 12:14 (CEST)
== [[n:Wikilivres passe la barre des 10 000 pages de contenu]] ==
[[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 4 mai 2011 à 21:30 (CEST)
:Il faudra fêter aussi les 400 livres : [[Wikilivres:Tous les livres|il n'en manque que huit]]. {{Sourire}}
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 5 mai 2011 à 01:42 (CEST)
::7... grâce à [[Utilisateur:Savant-fou|Savant-fou]] avec [[Histoire de France]] -- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 5 mai 2011 à 11:23 (CEST)
:::Oui, je préfèrerai qu'on se base plutôt sur ce compte là, bien plus représentatif de notre projet. Attention toutefois à ne pas faire de la ''publicité mensongère'' et à bien préciser que l'essentiel des contenus est en cours de rédaction. Toutefois, on peut ajouter que quelques livres sont prêt et mettre en avant la vitrine, comme l'a fait JackPotte. [[Utilisateur:Sub|Sub]] 5 mai 2011 à 20:03 (CEST)
::::6... grâce à [[Utilisateur:Jajard|Jajard]] avec [[Puredyne]] -- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 21 mai 2011 à 17:59 (CEST)
== [[:wikipedia:fr:Projet:Mardi c'est Wiki|Mardi c'est Wiki]] ! ==
Bonjour à tous !<br/>
Je viens présenter ici un projet qui démarre... Des rencontres hebdomadaires de Wikistes dans les villes de la francophonie.<br/>
C'est parti du succès de la NCO à Rennes qui apprécie de se rencontrer régulièrement.<br/>
On s'est dit avec Trizek : mais peut-être existent-ils plein d'autres Wikistes qui rêvent en secret de se rencontrer dans leur ville et qui n'en ont pas eu l'occasion ? C'est l'idée du [[:wikipedia:fr:Projet:Mardi c'est Wiki|Mardi c'est Wiki]] ! !<br/>
Est-ce que ça pourrait intéresser des contributeurs par ici ? [[Utilisateur:Plyd|Plyd]] 6 mai 2011 à 11:08 (CEST)
== Bourses Wikimédia France pour Wikimania ==
[[File:Haifa_wikimania_3.png|thumb|350px]]
Bonjour ! Wikimédia France attribue cette année encore des bourses pour contribuer au voyage vers la grande conférence annuelle wikimédienne, [[:wm2011:|Wikimania]], qui se tiendra cette année à Haïfa, en Israël, du 4 au 7 août 2011.
Pour ceux qui ne connaissent pas, vous pouvez regarder les [[:commons:Category:Wikimania 2010|photos de l'année dernière]], [[:commons:Category:Wikimania 2010 presentation slides|quelques présentations passées]], [[:wm2011:|le site de cette année]], et [[:w:Wikimania|l'article consacré sur Wikipédia]].
Les conditions d'attribution sont disponibles sur meta : '''[[:meta:Wikimédia France/Bourses Wikimania 2011]]'''.
N'hésitez pas à en parler à tous ceux qui pourraient être intéressés. Et ça serait bien de voir des Wikimédiens non-Wikipédiens et non-Commonistes à Wikimania, n'hésitez pas à postuler si ça vous intéresse et si vous pouvez vous libérer une semaine.
Pour Wikimédia France, ~ [[User:Seb35|Seb35]] 6 mai 2011 à 20:24 (CEST)
== Plus de barre d'édition! ==
la barre d'édition a litéralement diparu! Quand on ajoute un sujet, les mots "sujet" et "titre" sont aussi très écartés: <br /> "Sujet............/..........................titre".<br />
Les préférences n'ont plus d'onglet et tout est à la suite, ce qui est très pénible quand on veut valider puisque le bouton pour enregistrer les préférences est en bas... Est-ce-que c'est lié à une mise à jour du site. A un problème du côté de mon naviguateur. J'ai d'ailleurs essayé l'affichage de compatibilité, mais rien ne s'améliore... Y-a-t-il quelque chose à faire...<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 7 mai 2011 à 10:38 (CEST)
Les préférences sont revenus à la normale, mais pas de trace de barre d'édition. Il y a eu aussi des problèmes d'affichage. Plus aucune présentation. Juste des images et du texte. J'ai eu aussi un code ressemblant à du php ou du html, je ne sais pas trop...<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 7 mai 2011 à 10:52 (CEST)
:Cela ne vient pas du site (avec le même navigateur je ne vois rien), ni de tes sous-pages .css et .js. Si tu as bien [[Discussion_utilisateur:TouzaxA#Menu_.C3.A9dition|suivi la procédure habituelle]] et que cela persiste, c'est peut-être un module complémentaire de ton navigateur, peux-tu en tester un autre stp (je te conseille [[Firefox]], il permet de synchroniser automatiquement les favoris et mots de passes sur tous les PC/téléphones dans le monde). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 7 mai 2011 à 12:27 (CEST)
::Tout est revenu à la normale, et donc, il n'y pas que la barre d'édition qui reviens, puisque je reviens moi aussi. J'avais fait une pause: je n'arrivait pas à rédiger sans la barre et avec tant de problèmes de formatages. Donc me voilà revenu, je ne sais toujours pas d'où cela viens, mais tant que cela ne se reproduit pas...<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 11 mai 2011 à 14:54 (CEST)
== Importer [[:en:Template:Prerequisite]] ==
Ce modèle recommande la lecture d'un livre avant le courant, par exemple il faudrait connaître [[Le langage HTML|HTML]] avant de se lancer dans [[Le langage CSS|CSS]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 8 mai 2011 à 21:56 (CEST)
:Nous avons déjà {{m|lecture nécessaire}} et {{m|lecture conseillée}}. [[Utilisateur:Sub|Sub]] 9 mai 2011 à 00:16 (CEST)
::Merci, je vais ajouter les liens interwikis. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 9 mai 2011 à 00:18 (CEST)
:::Je crois qu'il y a encore des efforts à faire sur [[Wikilivres]], ces deux modèles y sont bien mentionnés. Si tu es passé à côté c'est qu'il faut peut-être revoir l'organisation du livre. [[Utilisateur:Sub|Sub]] 9 mai 2011 à 20:52 (CEST)
::::En effet {{m|Étiquette}} m'avait paru un peu abstrait en premier lecture au milieu d'une longue page d'aide, surtout que je ne l'ai pas revu depuis deux ans... Bref, c'est en forgeant qu'on devient forgeron. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 9 mai 2011 à 21:29 (CEST)
== Nouveaux modèles... ==
Bonjour à tous, j'ai décidé de créer de nouveaux modèles...<br />
*{{modèle|Patrouilleur}} pour signaler que l'on est patrouilleur.
*{{modèle|Mes modèles}} pour signaler la page où l'on a ses essais de modèles.
*{{modèle|Brouillons}} pour signaler la page où l'on peut trouver les brouillons de l'utilisateur.
J'attend vos commentaires, qu'ils soient bons ou mauvais...<br />
Bonne journée,<br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 12 mai 2011 à 17:21 (CEST)
:{{applau}} [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 mai 2011 à 21:33 (CEST)
::Merci. <br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion Utilisateur:TouzaxA|Discuter]].<br />Voir [[Blender, Quézaco ?]], le tutoriel sur Blender 2.57. <br /> Le 13 mai 2011 à 09:12 (CEST)
==[[Christiane Faure]]==
[[Christiane Faure]] est un copyvio du site [http://nec.pluribus.impar.over-blog.com/article-c-etait-a-monsieur-l-education-populaire-54178287.html nec.pluribus]...merci de ne pas l'exporter sur wikipédia --[[Spécial:Contributions/77.204.25.53|77.204.25.53]] 20 mai 2011 à 10:15 (CEST)
:{{fait}} Réglé par Savant-fou, doublon de [[Wikilivres:Pages_à_supprimer#Christiane_Faure|PàS]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 20 mai 2011 à 13:55 (CEST)
== [[Wikilivres:Recherche dans les catégories]] ==
J'ai importé un nouveau gadget qui fait fonctionner cette page, en attendant d'y ajouter l'option "catégories récursives", qu'en pensez-vous ? (on pourrait le mettre dans [[Mediawiki:Common.js]] et fusionner l'interface avec [[Wikilivres:Catégories]]). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 28 mai 2011 à 11:30 (CEST)
:Le gadget est utile, mais ne comporte pas toutes les catégories (pour l'instant limité aux recettes). Il faudrait aussi revoir la navigation dans les résultats et ne pas proposer de lien suivants sur la dernière page (en particulier quand il n'y a aucun résultat).
:OK pour l'intégrer globalement quand le gadget sera au point.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 28 mai 2011 à 13:18 (CEST)
::Nous pourrions maintenant le placer dans [[Livre de cuisine]] (avec les recettes uniquement) ? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 3 juillet 2011 à 14:26 (CEST)
:::Le problème semble corrigé. Donc OK pour l'ajout au livre de cuisine.
:::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 3 juillet 2011 à 14:44 (CEST)
::::Si vous faites ça, débrouillez-vous pour que ça se ''dégrade gracieusement'' comme on dit pour les gens qui n'ont pas le gadget activé (tous nos visiteurs anonymes). [[Utilisateur:Sub|Sub]] 3 juillet 2011 à 14:50 (CEST)
:::::Je pense à [[Mediawiki:Common.js]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 3 juillet 2011 à 16:12 (CEST)
{{fait}} [[Livre de cuisine]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 6 août 2011 à 11:47 (CEST)
:Super ! Deux remarques : ça devrait plutôt se trouver sur [[Recettes de cuisine]] et je vois un texte « (200 précédentes) (200 suivantes) (200 précédentes) (200 suivantes) » de trop. [[Utilisateur:Sub|Sub]] 6 août 2011 à 12:19 (CEST)
::Je laisse le deuxième point à Phe, mais sur le premier, actuellement les catégories de recettes à base d'alcool sont mélangées avec le livre de boisson. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 6 août 2011 à 13:26 (CEST)
== Gluten ==
Déplacé vers [[Discussion:Recettes de cuisine#Gluten]]
== Wikimédia France ouvre deux postes au recrutement ==
*Un(e) chargé(e) de mission communauté/technologie pour l'animation et l'appui aux projets de l’association : CDI avec un salaire brut de 30 000 à 32 000 €.
*Un(e) chef de projet pour la gestion de sa levée de fonds 2011 : CDD de 5 mois à partir de septembre 2011, salaire à négocier suivant profil dont 15% de variable sur objectifs.
Le texte complet des appels à candidatures et la marche à suivre sont disponibles : http://www.wikimedia.fr/recrutement
[[Utilisateur:Crochet.david|Crochet.david]] 9 juin 2011 à 22:37 (CEST)
== Héberger des code-sources ==
Je suis en train de créer des projets d'exemple pour [[Introduction au test logiciel]], je me retrouve ainsi avec une hiérarchie de fichier que le lecteur devrait pouvoir récupérer simplement. On pourrait imaginer, que selon le principe wiki, l'utilisateur puisse proposer des corrections. Quel serait selon vous, la meilleure solution pour proposer ces codes ? On pourrait bien sûr penser à une forge de logiciel libre mais laquelle ? Le toolserver propose aussi l'hébergement. Faudrait-il utiliser un gestionnaire de version tel Git qui permettrait aux utilisateurs de proposer des corrections à inclure ? Comment mutualiser pour regrouper les codes sources des différents livres afin d'éviter aux contributeurs d'avoir à créer des comptes sur 36 forges/projets différents ? [[Utilisateur:Sub|Sub]] 12 juin 2011 à 15:40 (CEST)
:[[tools:~jackpotte/unicode-HTML.html|Comme ça]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 juin 2011 à 16:08 (CEST)
::Et comment, en tant que lecteur, je peux récupérer le code source ? Est-ce que je peux proposer des corrections, faut-il un compte sur le toolserver ? [[Utilisateur:Sub|Sub]] 12 juin 2011 à 16:15 (CEST)
:::Oui et ce n'est pas fait vraiment fait pour ça, même [[:commons:Commons:Critères_d'inclusion#.C3.8Atre_un_fichier_multim.C3.A9dia|Commons]] refuse. Il faudrait creuser du côté de [[:Incubator:]] pour développer [[:meta:Category:JavaScript|ce que Meta fait difficilement]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 juin 2011 à 16:51 (CEST)
== Pages à recycler ou jeter ==
Quelques pages à recycler ou supprimer : [[Spécial:Index/Accueil]].
Il y a des pages de contenu à placer dans certains livres.
Mais que fait-on des anciennes pages de portails/étagères ?
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 13 juin 2011 à 11:16 (CEST)
:Archivons-les, pour [{{fullurl:Accueil/Informatique/Rayon_ASR|action=edit}} ce genre de liste] elles seront remplacées par des catégories. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 juin 2011 à 11:34 (CEST)
:Suppression immédiate. [[Utilisateur:Sub|Sub]] 13 juin 2011 à 12:32 (CEST)
:Archivage. [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 21 juin 2011 à 14:11 (CEST)
::DavidL, tu hésites ? Cela traîne. Dans quelques jours, je rase {{;)}}. [[Utilisateur:Sub|Sub]] 5 septembre 2011 à 20:06 (CEST)
:::[[wikt:demain on rase gratis|...gratis ?]] {{Mdr}}
:::J'attends de l'aide pour faire le tri des déchets... et laisser le temps à certains de fouiller dans les poubelles pour récupérer des choses [[File:Recycle.jpg|100px]].
:::Il faut également supprimer toutes les pages de [[:Catégorie:Étagères de Wikilivres]].
:::Un bot serait très utile.
:::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 5 septembre 2011 à 22:34 (CEST)
== Tournoi d'échec d'Anchin ==
Bonjour à tous. [[w:Liste des Participants au Tournoi de Pecquencourt dit d'Anchin en 1096|Cette page]] est-elle susceptible de vous intéresser ? -- [[Utilisateur:Quentinv57|Quentinv57]] [[Discussion_Utilisateur:Quentinv57|<strong><span style="color:#990000">✍</span></strong>]] 16 juillet 2011 à 09:54 (CEST)
:Les historiques [[w:Catégorie:Concours|des concours]] sont sous le monopole de WP, on ne va pas récupérer que les plus petits. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 16 juillet 2011 à 10:08 (CEST)
::Oui, y'a un côté « Wikibooks = Poubelle de Wikipédia ». [[Utilisateur:Sub|Sub]] 16 juillet 2011 à 10:10 (CEST)
:Je dirai, pas le moins du monde. C'est trop anecdotique, à la rigueur si nous avions un spécialiste de l'histoire des échecs mais ce n'est pas le cas... [[Utilisateur:Sub|Sub]] 16 juillet 2011 à 10:10 (CEST)
:::C'est vrai... [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 16 juillet 2011 à 11:53 (CEST)
::Pas de contenu pédagogique + page isolée ne pouvant se rattacher à aucun livre existant + (Wikibooks=récupération) = Pas besoin de cette page.
::La question ne devrait même pas être posée pour aucune page. Les contributeurs de Wikipédia qui veulent transférer le contenu d'une page ici devraient faire l'effort de vérifier si le contenu a un réel intérêt pédagogique et de venir sur ce site et rechercher eux-mêmes le livre approprié.
::La question qui devrait être posée serait plutôt du genre : "Est-ce que <u>cette page de wikipédia</u> pourrait être ajouté à <u>ce livre de wikibooks</u> ?".
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 16 juillet 2011 à 11:59 (CEST)
== Vandalismes sur [[Apprendre la guitare/Suggestions de morceaux]] ==
Bonjour. Cette page a été victime de vandalismes ce matin par les IP suivantes : {{u|78.122.245.232}}, {{u|82.241.220.228}}, {{u|82.246.147.162}}, {{u|77.195.88.181}} et {{u|82.237.76.88}}. Ces IP sont des vandales noëlistes ([http://www.jeuxvideo.com/forums/1-50-75643223-1-0-1-0-omg-cette-page-wiki.htm voir ici]) qui sévissent depuis trop longtemps sur Wikipédia et ils m'ont l'air de se rabattre vers d'autres wiki, en raison du fait que nous ne leur faisons plus de cadeaux quand ils débarquent sur WP : blocage immédiat. J'ignore comment vous réagissez face à eux ou si vous avez déjà été confrontés aux noëlistes, mais méfiez-vous et je vous conseille d'adopter dans vos patrouilles la même démarche que sur WP face à ce genre de vandales. Vous pouvez être certains qu'ils ne lâcheront pas le morceau et qu'ils reviendront pour vandaliser, à moins que vous leur montriez que vous n'en voulez pas et que vous êtes prêts les bloquer dès qu'ils apparaissent.<br/>
Les vandalismes ont par ailleurs été effacés par une IP bienveillante (la dernière dans l'historique). [[Utilisateur:Juraastro|Juraastro]] 20 juillet 2011 à 12:03 (CEST).
:Merci, ici toutes les modifications sont patrouillées sous 48 h donc ils ne resteront pas longtemps. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 20 juillet 2011 à 12:11 (CEST)
== Gestion des {{NUMBEROFFILES}} fichiers du site ==
Conformément à la politique de la fondation, il faudrait déplacer [{{fullurl:Spécial:Toutes les pages|from=&to=&namespace=6}} tous ces médias] sur Commons (je pense avec [[:Commons:Commons:Tools/Commonist|Commonist]]). Avec cependant une possibilité de suppression pour ceux que [[Spécial:Fichiers inutilisés]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 23 juillet 2011 à 16:19 (CEST)
:Il n'y a pas de politique pour ce transfert total vers commons. Il s'agit simplement d'une recommandation générale pour la plupart des images.
:Certains fichiers locaux ne peuvent pas être transférés:
:* Les droits et licenses sont différents sur commons (ex: Fair use sur les sites anglophones mais pas sur commons),
:* Certains fichiers n'ont aucun intérêt à être sur commons (ex: logo localisé de wikibook, illustration de gadgets locaux à un seul site, ...),
:* Les fichiers sont supprimés de commons sans avertissement à propos d'un vote de suppression.
:Concernant les fichiers inutilisés, ils ne doivent pas être supprimés mais devraient plutôt être transférés sur commons afin d'être utilisés sur un autre wiki.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 23 juillet 2011 à 17:38 (CEST)
::Rappelons-nous qu'à l'époque, Commons fut créé après Wikipédia et il a fallu rectifier le tir en faisant quantités d'export des différentes Wikipédia vers Commons. Aujourd'hui, il me semble évident que Commons existant dès l'origine de notre projet, nous devrions '''désactiver l'import de fichiers sur Wikibooks''', ou le réserver aux administrateurs (car, comme le mentionne DavidL, il y a des exceptions justifiées à n'avoir que certains fichiers ici). Cela favoriserait la coopération entre les projets et nous éviterait pas mal de soucis : fichiers différents avec le même nom ici et là-bas... surveillance des fichiers sans licence, gestion centralisée des cessions de droits (ORTS), classification des fichiers, utilisateurs néophytes qui importent ici au lieu de Commons, etc. Ainsi, on délègue tout ça aux contributeurs de Commons, qui sont sûrement bien plus expérimentés et outillés que nous. [[Utilisateur:Sub|Sub]] 24 juillet 2011 à 12:34 (CEST)
:::Oui, j'ai d'ailleurs un petit problème avec Commons... Il n'accepte plus les logos de Blender. Ni les dérivés. Les fichiers ont été supprimés. Donc pour Blender, Quézaco ? et ma signature... [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 25 juillet 2011 à 09:20 (CEST)
Merci, je vais essayer de trier en connaissance de cause. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 août 2011 à 22:50 (CEST)
== Page de description des fichiers sur commons ==
Bonjour,
Quand un fichier est sur commons (ex: [[:Fichier:Produire un wiki avec MediaWiki.svg]]), on lit sa page de description sur wikibooks, et un lien passant inaperçu affiche un lien vers commons.
Cependant, le fait d'avoir cet accès depuis notre site n'est pas utile et source d'erreurs :
* La page locale peut être modifiée, ce qui est totalement inutile : la description devant être modifiée sur commons, pas en local.
* Télécharger une nouvelle version localement écrase (occulte en fait) l'image existant sur commons. Je viens de faire l'erreur pour le fichier que je donne en exemple.
Je propose donc de rediriger automatiquement vers commons, afin d'éviter toute erreur.
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 juillet 2011 à 15:41 (CEST)
: Il faudrait déjà commencer par rendre le lien vers Commons plus lisible ; en l'état il ne saute pas vraiment aux yeux... Comparez avec Wikipédia : [[w:Fichier:Produire un wiki avec MediaWiki.svg]]. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 26 juillet 2011 à 16:14 (CEST)
::{{Fait}} Fait, mais cela n'éviterait pas toutes les erreurs vu que la visibilité d'un élément est plus suggestive qu'universelle. Ce que l'on voit essentiellement c'est l'image, ou le long tableau d'historique des versions.
::Vu que la page locale de description ne sert qu'à se tromper, je propose de rediriger vers commons afin de prévenir toute erreur.
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 juillet 2011 à 16:25 (CEST)
:::Aucune objection. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 26 juillet 2011 à 23:33 (CEST)
:::Tout à fait d'accord avec DavidL et Darkdadaah. [[Utilisateur:Sub|Sub]] 26 juillet 2011 à 23:41 (CEST)
::::{{Fait}} Vu l'unanimité actuelle, j'ai mis en place la redirection automatique.
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 02:37 (CEST)
== Logo Wikibooks ==
Bonjour,
Alors que je sauvegardais un article, il s'est passé des choses bizarres et le logo wikibooks a été remplacé par celui de la wikipédia anglaise... Certains liens ne fonctionnent plus, par exemple celui qui permet d'ajouter une rubrique au bistro. Pour le coup, j'ai un peu de mal à comprendre ce qui a pu se passer...
Amitiés, [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] 27 juillet 2011 à 14:33 (CEST)
:Et le lien [[http://fr.wikibooks.org/wiki/]] aboutit à la page Accueil sur wikipédia !
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 14:37 (CEST)
::Et j'ai des pbs pour modifier une section -> "Erreur mauvais titre", généré sur wikipédia !
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 14:38 (CEST)*
:::J'ai testé avec un programme de téléchargement HTTP :
:::* Pour le lien [[http://fr.wikibooks.org/wiki/]], la réponse HTTP est 300 (redirect) vers wikipédia !
:::* Cela ne vient pas de ma modif pour les pages de descriptions de fichiers où le comportement est différent, réponse 200, redirection avec du Javascript (pas HTTP).
:::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 14:49 (CEST)
::::Votez pour le bug si vous avez un compte : [https://bugzilla.wikimedia.org/show_bug.cgi?id=30079 Bug 30079 sur bugzilla]
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 15:24 (CEST)
:::::Le problème semble déjà résolu.
:::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 juillet 2011 à 15:25 (CEST)
: Bonjour, c'était un bug qui affectait tous les Wikis (sauf Wikipédia qui est le wiki par défaut), le problème étant plus général. C'est résolu à présent. [[Utilisateur:Darkdadaah|Dakdada]] [[Discussion Utilisateur:Darkdadaah|<small>(discuter)</small>]] 27 juillet 2011 à 16:31 (CEST)
== Refonte des pages de recette : usage d'un super modèle, formulaire et sémantique web ==
Discussion déplacée vers [[Discuter:Recettes de cuisine]]. [[Utilisateur:Sub|Sub]] 30 juillet 2011 à 11:05 (CEST)
== Astuces de perfectionnements ==
Bonjour à tous.
Je me permets de vous demander si il est possible de compléter un wikibook avec des astuces permettant de perfectionner une technique.
Par exemple, dans ce [[Photographie/Techniques_scientifiques/Astrophotographie|wikibook]], j'aurais aimé y placer une sorte de tutoriel permettant la photo du ciel nocturne avec un apn et un trépied.
[[Photographie/Techniques_scientifiques/Photographie_rapprochée|Ici]], j'aurais aimé parler du [[w:fr:focus stacking|focus stacking]] permettant l'extension de la profondeur de champ.
--[[Utilisateur:ComputerHotline|ComputerHotline]] 1 août 2011 à 10:51 (CEST)
:Bonjour ComputerHotline,
:Oui c'est possible. En cas d'hésitation pour l'organisation (sous quelle forme insérer le texte par exemple), il faut en discuter avec l'auteur principal : [[Discussion utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]].
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 1 août 2011 à 11:06 (CEST)
::Ou demander directement sur la page de discussion correspondante... [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 1 août 2011 à 13:53 (CEST)
:::Pour ce qui est du focus stacking, le sujet est abordé ici : [[Photographie/Netteté des images/Profondeur de champ/Considérations pratiques#Profondeur de champ et combinaison d'images|Profondeur de champ et combinaison d'images]]. -- [[Utilisateur:Nicolas DEJARDIN|Nicolas DEJARDIN]] 1 août 2011 à 19:16 (CEST)
Bonjour,
Toutes les interventions permettant de compléter le wikilivre de photographie sont non seulement accueillies avec plaisir, mais encore vivement souhaitées. Il faut simplement respecter la logique d'organisation des chapitres et voir ensemble comment l'améliorer lorsque l'évolution des techniques rend certains changements nécessaires, ce que je fais assez souvent d'ailleurs, au fur et à mesure de la rédaction.
Le ''focus stacking'' a été mis dans la page consacrée à la profondeur de champ car il ne concerne pas seulement la photographie rapprochée, loin de là !
Amitiés. [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] 2 août 2011 à 11:27 (CEST)
== 400 livres ! ==
Bonjour à tous. Je vous informe que nous avons atteint hier la barre des 400 livres, grâce au nouveau livre [[CASES]]. C'est une très bonne nouvelle, car cela montre que Wikibooks s'est construit une communauté qui développe activement de nouveaux livres sur des sujets encore inexplorés. Bravo à tous les contribueurs, et espérons qu'il y en aura d'autres qui nous rejoinderons dans les semaines, les mois à venir ! <br />
[[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 1 août 2011 à 14:03 (CEST)
:Et que ce soit [[WL:PàS#Essai_de_prospective_environnementale._2040.2C_nord_de_la_France...|pédagogique]] et non redondant [[w:Wikipédia:Critères d'admissibilité des articles|des projets frères]] ! [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 1 août 2011 à 19:54 (CEST)
::Pardon ! J'avais oublié ces points là. (Il me semblait bien avoir oublié quelque chose...) [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 2 août 2011 à 08:32 (CEST)
== Supprimer la page [[Modèle:CASES]] ==
J'ai créé cette page avant de m'apercevoir que la sous page [[CASES/Sommaire]] était déjà utilisée pour cela. Quelqu'un peut-il l'effacer ? --[[Utilisateur:Rical|Rical]] 2 août 2011 à 16:57 (CEST)
:Suppr. [[Utilisateur:Greudin|Greudin]] 2 août 2011 à 17:46 (CEST)
::Merci. --[[Utilisateur:Rical|Rical]] 2 août 2011 à 18:20 (CEST)
== [[Wikijunior:Lego]] ==
Bonjour à tous, j'ai un petit souci avec ce modèle, il renvoi vers les historiques de Wikiversity anglais ; {{Traduit de|en|LEGO Design|02/08/2009}}
Ensuite il y a ces deux pages ; [[:en:Robotics/Exotic Robots/The LEGO World]] et [[:en:Mindstorms Robotics]], je peux les mettre aussi dans le wikijunior ? Merci beaucoup pour vos avis, amcalement. [[Utilisateur:FrankyLeRoutier|FrankyLeRoutier]] % [[Discussion utilisateur:FrankyLeRoutier|Service après-vente]] 6 août 2011 à 14:25 (CEST)
:{{fait}} Modèle corrigé (après vérification des pages liées). Pour le reste tu as le feu vert. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 6 août 2011 à 17:08 (CEST)
== Demande de modification sur [[Mediawiki:Geshi.css]] ==
Je souhaiterai modifier la page [[Mediawiki:Geshi.css]] afin de corriger l'affichage des balises <code><nowiki><syntaxhighlight></nowiki></code> : ajouter un cadre et un fond comme sur les balises <code><nowiki><pre></nowiki></code>, et rétablir la taille normale (comme sur Wikipédia & Wikiversité).
L'ensemble des modifications — minimes — se trouve sur cette page : [[Utilisateur:Cynddl/BàS]].
Cordialement, [[Utilisateur:Cynddl|<span style="color:#2e586a">Cynddl</span>]] <sup>[[Discussion Utilisateur:Cynddl|<span style="color:#2e586a">( ⌧ )</span>]]</sup> 8 août 2011 à 13:36 (CEST)
:J'ai importé [[MediaWiki:Geshi.css]], maintenant les caractères sont plus grands. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 8 août 2011 à 20:12 (CEST)
::Merci ! Il reste à modifier la dernière section, c'est à dire enlever :
<div style="padding-left:4em"><syntaxhighlight lang="css">
body.skin-chick div.mw-geshi,
body.skin-monobook div.mw-geshi,
body.skin-vector div.mw-geshi {
padding: 1em;
border: 1px dashed #2f6fab;
color: black;
background-color: #f9f9f9;
line-height: 1.1em;
}
</syntaxhighlight></div>
::et mettre à la place :
<div style="padding-left:4em"><syntaxhighlight lang="css">
div.mw-geshi {
border: solid 1px #dddddd;
background: #f6f6f6;
padding: 1em;
line-height: 1.1em;
color: black;
}
</syntaxhighlight></div>
::[[Utilisateur:Cynddl|<span style="color:#2e586a">Cynddl</span>]] <sup>[[Discussion Utilisateur:Cynddl|<span style="color:#2e586a">( ⌧ )</span>]]</sup> 8 août 2011 à 21:25 (CEST)
:::Pardon mais j'ai du mal à saisir pourquoi nos balises n'auraient pas les mêmes couleurs que celles de Wikipédia... [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 8 août 2011 à 23:01 (CEST)
::::L'intérêt, c'est d'uniformiser l'apparence entre :
bidule
:et :
<syntaxhighlight lang="text">bidule</syntaxhighlight>
:en privilégiant la première « boîte ». [[Utilisateur:Cynddl|<span style="color:#2e586a">Cynddl</span>]] <sup>[[Discussion Utilisateur:Cynddl|<span style="color:#2e586a">( ⌧ )</span>]]</sup> 9 août 2011 à 01:14 (CEST)
::Comme vu sur l'IRC avec toi, le style actuel était en fait déjà optimisé. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 août 2011 à 10:48 (CEST)
== Problèmes avec la version sécurisée de notre site ==
Je viens de corriger le modèle {{m|lien modifier}}. En effet, il n'était pas fonctionnel étant donné que l'adresse « fr.wikibooks.org/wiki » était écrite en dur dans le modèle. Pour rappel, cette adresse n'est pas systématiquement celle utilisée pour accéder à notre projet, par exemple, la [https://secure.wikimedia.org/wikibooks/fr/ version sécurisée du site est accessible à https://secure.wikimedia.org/wikibooks/fr/]. J'ai donc fait une correction en utilisant la syntaxe ''fullurl:Page concernée''. J'ai constaté d'autres problèmes en utilisant la version sécurisée du site :
* Le lien « purger » (ajouter par un gadget il me semble) ne fonctionne pas
* Lorsqu'on clique sur « Ajouter un message » sur le bistro, un message de confirmation s'affiche, un clic sur OK nous renvoie sur la version non sécurisée et pas en mode modification comme espéré
* On est plus redirigé sur Commons quand on clic « Importer image ou son » dans le menu à gauche
Les personnes qui ont ajoutés ces fonctionnalités pourrait-elle faire les modifs ?
Ce serait bien que d'autres utilisateurs essaient de contribuer via la version sécurisée du site pour voir si on a d'autres problèmes de ce genre. [[Utilisateur:Sub|Sub]] 26 août 2011 à 13:15 (CEST)
:Je vais effectuer les modifications nécessaires.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 août 2011 à 14:01 (CEST)
::J'ai effectué des modifications dans les scripts.
::Il faut donc recharger les scripts : Ctrl+F5 / Ctrl+Shift+F5 / Ctrl+R / Ctrl+Shift+R.
::'''Pour les développeurs et ceux qui importent des scripts/gadgets :'''
::J'ai ajouté les paramètres <code>wgServerName</code> (fr.wikibooks.org en général), <code>wgProtocol</code> (http, https, ...) et une fonction <code>fullurl(''nom_de_page, paramètres...'')</code> fonctionnant comme <code><nowiki>{{fullurl:nom_de_page|paramètres...}}</nowiki></code>.
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 août 2011 à 16:29 (CEST)
:::Bonne idée. Merci, [[Utilisateur:Sub|Sub]] 26 août 2011 à 20:47 (CEST)
::::J'ai corrigé les gadgets et j'en ai ajouté un autre pour corriger les liens vers les autres sites Wikimedia (wikipédia, wikiversité, commons) afin de conserver la version sécurisée en HTTPS et éviter le basculement en HTTP non sécurisé :
::::* {{MediaWiki:Gadget-FixLinks}}
::::Cependant, il faudrait certainement plutôt l'intégrer au script [[MediaWiki:Common.js]].
::::<s>Toutefois ce script ne peut pas agir sur les redirections.</s> Les redirections wiki (# REDIRECT) semblent OK. Le script corrige les redirections qui changent le protocole en HTTP (liens inter-projets) en les évitant en donnant un lien HTTPS direct.
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 12 septembre 2011 à 22:47 (CEST)
:::::J'ai amélioré la fonction fullurl pour supporter les liens inter-project et internationaux. Il y a également une fonction localurl plus simple pour générer une URL relative à ce site.
:::::Il reste à déterminer s'il faut intégrer [[MediaWiki:Gadget-FixLinks]] à [[MediaWiki:Common.js]]. Il semble fonctionner correctement pour tous les liens possibles. À mon avis, on devrait l'intégrer (aucun intérêt de devoir revenir au HTTP quand on change de projet) comme l'ont déjà fait les wikipédia en anglais, et en français (MediaWiki:Common.js/secure.js).
:::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 19 septembre 2011 à 13:00 (CEST)
::::::En effet, je ne pense pas que cela plombe les temps de chargements de ceux qui restent en HTTP. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 19 septembre 2011 à 22:30 (CEST)
:::::::{{Fait}} Fait.
:::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 20 septembre 2011 à 01:28 (CEST)
::::::::Avec la nouvelle version de MediaWiki, le script n'est plus nécessaire, vu que maintenant les serveurs sont les mêmes :
::::::::* http://fr.wikibooks.org/
::::::::* https://fr.wikibooks.org/
::::::::S'il y en a, il faudra remplacer les liens absolus vers les serveurs MediaWiki pour supprimer le <code>http:</code> dans les URLs.
::::::::<code><nowiki>{{</nowiki>fullurl:Wikilivres:Le Bistro/Messages actuels}}</code> = {{fullurl:Wikilivres:Le Bistro/Messages actuels}}
::::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 1 octobre 2011 à 18:42 (CEST)
----
La version HTTPS semble fonctionner correctement, excepté pour le message du questionnaire sur la version mobile de wikipédia.
Ce message n'étant pas modifiable localement, la seule chose possible à faire est de voter pour ce bug : [[bugzilla:31446]].
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 13 octobre 2011 à 17:54 (CEST)
----
Le problème précédent étant réglé, Wikibooks en français est maintenant l'un des rares projets de MediaWiki à n'avoir plus aucun problème de sécurité en mode HTTPS : le navigateur firefox affiche le nom du domaine (wikibooks.org) avant l'adresse complète du site.
Ce n'est pas le cas sur la plupart des autres projets (quel que soit la langue) : wikipédia, wikinews, ...
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 25 octobre 2011 à 14:00 (CEST)
== Remaniement de l'aide wikilivres ==
Wikilivres possède des pages d'aides réparties dans différents espaces de nom, ce qui peut contribuer à la confusion pour les nouveaux utilisateurs et donne un sentiment d'incohérence :
* L'espace de nom Aide:...
* L'espace de nom Wikilivres:...
* Le livre '''[[Wikilivres]]''' dans l'espace de nom principal.
Et le comble :
* '''[[Aide:Comment démarrer un livre]]''' redirige vers '''[[Wikilivres/Démarrer un wikilivre]]'''.
* '''[[Wikilivres:Comment démarrer une page]]''' redirige vers '''[[Aide:Comment démarrer une page]]'''.
Il faut donc réorganiser toutes ces pages en commençant par les placer dans un seul espace de noms.
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 2 septembre 2011 à 16:56 (CEST)
:Je propose de ne pas réinventer une norme dans notre coin et d'ajouter des interwikis (étrangers ou avec {{m|Autres projets}}) pour consolider la logique. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 2 septembre 2011 à 19:40 (CEST)
:De mon point de vue, la rédaction de ces pages ne sert pas à grand chose et d'ailleurs tout reste à faire (énormément de choses pas à jour), il est trop difficile de structurer tout ça (qui arrive à s'y retrouver dans l'aide de Wikipédia ?). J'ai donc pris l'initiative de [[Wikilivres]], qui autant que possible suit la méthode habituelle pour écrire des livres ici (on réinvente pas la roue dans l'espace Aide:), un bon vieux wikibook : ça, on sait faire. Je constate que l'essentiel de [[Wikilivres]] est à jour alors que l'essentiel de ''Aide'' ne l'est pas et je crois bien que l'approche minimaliste et simplifiée de [[Wikilivres]] y est pour beaucoup. En effet, [[Wikilivres]] ne traite que des aspects propres au projet francophone, toute la documentation technique (écrire un modèle, syntaxe wiki), est composé de liens vers la documentation de MediaWiki ou vers meta:. Pour moi, il faut presque supprimer l'espace Aide: où le réduire à une page qui indique que l'info se trouve dans [[Wikilivres]] et se concentrer sur l'amélioration de ce livre qui aussi l'avantage d'être imprimable, contrairement à l'aide. Quant à l'espace de nom Wikilivres:, j'ai également constaté des incohérences, certaines pages sont plutôt des pages d'aide alors que cet espace de nom serait plutôt pour les choses autour des livres (la communauté, le bistro...) [[Utilisateur:Sub|Sub]] 2 septembre 2011 à 21:11 (CEST)
::Oui, pour moi, il faudrait déplacer toutes les pages d'aide vers l'espace de nom "Aide". C'est la façon la plus compréhensible et cohérente que l'on puisse faire. Je suis d'accord avec Sub sur les pages Wikilivres:. C'est un petit peu tout et n'importe quoi. C'est un peu l'espace "fourre-tout"... [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 3 septembre 2011 à 10:37 (CEST)
== Verbe "suffire" ==
Dans le poème de Henri de Régnier il y a une faute d'orthographe.
Le participe passé du verbe "suffire" est invariable, c'est "suffi". Le poème s'écrit
"Un petit roseau m'a suffi"
Sur votre site, vous mettez un "t" à la fin du participe passé.
Merci de corriger
Yves Bordet
:{{Fait}} C'est corrigé.
:Il aurait fallu donner [[Français en classe de cinquième/Le champ lexical|le lien vers la page]]. De plus il s'agit d'un wiki. Donc chaque page peut être modifiée si besoin (onglet "modifier" en haut).
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 10 septembre 2011 à 03:43 (CEST)
== Journal de debuggage ==
Depuis quelque temps j'obtiens le message suivant sur fond mauve lorsque je veux modifier une page ou un chapitre :
Journal de debuggage
ERROR in function setupTitle: TypeError: Cannot convert 'subpages[0]' to object
ERROR in function LienUpload: TypeError: Cannot convert 'info' to object
Je n'arrive pas à savoir d'où viennent ces erreurs; elles se produisent avec navigateurs Opera, Mozilla, (je n'ai pas essayé les autres)
Avez-vous une idée ?<br />
--[[Utilisateur:Goelette Cardabela|Cardabela]] 27 septembre 2011 à 17:34 (CEST)
:Je n'ai jamais vu cela : est-ce que stp cela survient au moment de la sauvegarde et quels gadgets as-tu coché ? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 27 septembre 2011 à 19:27 (CEST)
::Pour ne plus voir ces erreurs, tu peux faire ceci :
::* Aller sur [[Spécial:Préférences#preftab-8|préférence, onglet gadgets]].
::* Décocher la case en face de "Afficher la fenêtre du journal de déboggage des scripts (voir la documentation)." dans la rubrique "Développeurs".
::* Cliquer le bouton "Enregistrer les préférences" en bas de la page.
::Personnellement, j'ai le gadget coché, mais je ne vois aucune erreur sous Firefox 6, ou IE9.
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 septembre 2011 à 20:14 (CEST)
:::C'est normal que je ne vois pas la première erreur vu que j'utilise le gadget "Affichage amélioré des titres (alternative à la fonction par défaut)".
:::Maintenant que j'ai décoché le gadget, la coupure des titres au niveau des slash ne se fait plus, mais toujours pas de message d'erreur.
:::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 septembre 2011 à 20:21 (CEST)
::::J'obtiens une variante du premier message d'erreur quand je vais sur la page [[LaTeX]] (sans modifier) :
ERROR in function setupTitle: TypeError: subpages[0] is undefined
::::J'ai corrigé le problème : le message ne m'apparait plus.
::::Pas d'indice pour le second message d'erreur car rien ne s'appelle "info" dans la fonction LienUpload en question.
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 septembre 2011 à 21:41 (CEST)
:::::info existe dans fullurl (fonction appelée par LienUpload.
:::::<strong>'''Question :''' Quand l'erreur apparait, qu'est-ce qui s'affiche après avoir tapé ce qui suit dans la barre d'adresse puis la touche entrée ?</strong>
:::::<code>javascript:wgProtocol</code>
:::::Normalement ce soit être http ou https ('''tout en minuscule'''). Sinon cela peut effectivement provoquer le 2nd message d'erreur.
:::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 27 septembre 2011 à 21:52 (CEST)
:'''Problème résolu pour moi :''' En décochant "Afficher la fenêtre du journal de déboggage des scripts (voir la documentation)." dans la rubrique "Développeurs" de mes préférences le journal de débogage n'apparaît plus. Merci David.
:Il doit tout de même y avoir des erreurs car ces messages n'apparaissaient pas lors de mes dernières contributions ... ?
:J'oubliais ; Quelle barre d'adresse ? Dans le dernier message.
:--[[Utilisateur:Goelette Cardabela|Cardabela]] 28 septembre 2011 à 10:52 (CEST)
::Il s'agit de la barre d'adresse du navigateur internet.
::Normalement, l'adresse doit commencer par http:// ou bien par https://
::Si ce n'est pas le cas, alors effectivement cela peut causer des problèmes.
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 28 septembre 2011 à 11:09 (CEST)
:::Bon, OK, merci David. J'essaierai après avoir recoché "Afficher la fenêtre du journal de déboggage".
:::--[[Utilisateur:Goelette Cardabela|Cardabela]] 28 septembre 2011 à 13:31 (CEST)
::::Le second message d'erreur ne m'apparaissait pas car j'utilise la version sécurisée (HTTPS) du site.
::::Je viens de corriger le problème qui venait de la nouvelle version/configuration de MediaWiki où la variable wgServer commence par // sans mentionner le protocole http:.
::::Le message d'erreur ne devrait plus apparaitre.
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 1 octobre 2011 à 15:50 (CEST)
== message à revoir ==
[[Discussion MediaWiki:Wikimedia-copyrightwarning|déplacé]]
== Barre d'outils de modification ==
Bonjour,
La barre d'outils a été modifiée récemment et donne des résultats bizarres. Le chargement d'une galerie d'images fait apparaître divers « \n » qui fichent le bazar :
<pre>
\n<gallery>\nImage:M63.jpg|[[M63]]\nImage:Mona Lisa.jpg|[[La Joconde]]\nImage:Truite arc-en-ciel.jpg|Une [[truite]]\n</gallery>
</pre>
Voilà ce que ça donne :
\n<gallery>\nImage:M63.jpg|[[M63]]\nImage:Mona Lisa.jpg|[[La Joconde]]\nImage:Truite arc-en-ciel.jpg|Une [[truite]]\n</gallery>
Quelqu'un pourrait-il remettre les choses dans l'état initial ?
Amitiés, [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] 11 octobre 2011 à 00:56 (CEST)
:{{Fait}} Fait. Il y a avait le même problème avec les listes, et la section Références.
:Le script était OK pour MediaWiki 1.17, mais ne l'était plus pour la [[Spécial:Version|version 1.18]].
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 11 octobre 2011 à 11:50 (CEST)
::Tout à fait, cela a impacté tous les wikis plus ou moins par surprise, avec [[w:Wikipédia:Le_Bistro/5_octobre_2011#Mediawiki_1.18.2C_c.27est_d.C3.A8s_maintenant|des retours de bugs]] plus ou moins [[wikt:Wiktionnaire:Wikidémie#MW_1.18_et_Sp.C3.A9cial:Recherche|dispersés]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 11 octobre 2011 à 20:35 (CEST)
:::: Merci ! [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] 11 octobre 2011 à 23:58 (CEST)
== à propos des forces de marées : [[w:Force de marée]] ==
L'explication donnée pour former 2 bosses de marée diamétralement opposées ainsi que son illustration par une soustraction magique est totalement erronée ([[w:Force de marée]]).
Si on comprend bien l'existence de la bosse en face de la lune, en déduire celle opposée par différence et la faire ainsi gonfler est totalement irréaliste. Car il y a un phénomène dans la rotation du couple Terre-Lune, bien documenté dans l'article Wiki consacré, qui est oublié ici : c'est que "le centre de masse (ou dit de gravité) du couple se situe à 4671 km du centre de la Terre, quelques 1700 km sous la surface". De ce fait, le couple Terre-Lune tourne autour de cet axe perpendiculaire au plan de sa trajectoire et la Terre se comporte comme un excentrique dont le centre a une trajectoire sinueuse autour de celle du centre de masse qui suit ,lui, la trajectoire elliptique dans le plan de l'écliptique (voir l'excellente illustration dans le même article Terre-Lune de Wiki). Ainsi l'accélération qui en résulte pour les masses d'eau océaniques sont DIFFERENTES d'un côté à l'autre de l'axe Terre-Lune. Il en résulte ainsi réellement une accélération due à la rotation de la terre qui crée la seconde bosse de marée à l'opposé.
C'est la seule explication pédagogique qui tienne la route, toutes les autres, masquées sous de savants calculs (probablement exacts au demeurant) ne peuvent en rendre compte.
Ayant fait les frais d'une de ces secondes marées (généralement inférieures à la première) en restant échoué (volontaire) un jour de plus et ne déjaugeant qu'à celle plus forte, quelque 12 h plus tard, j'ai cherché une explication que je n'ai trouvée satisfaisante que dans un petit traité sur les marées, dont les références sont les suivantes :
Comprendre les marées , par Odile Guérin
2007, Editions Ouest-France, 72 pages (disponible sur Amazon ou autres vendeurs pour environ 6€).
Je ne suis pas en mesure actuellement de contribuer personnellement, mais j'apporte au "Bistrot" cette information que je considère fondamentale pour ne pas passer à coté de la réalité du phénomène et ne pas s'embarquer dans des explications fantaisistes d'ailleurs pompées dans de nombreux ouvrages géographiques qui évoquent généralement trop brièvement le sujet.
En revanche, si les rédacteurs expérimentés désirent me contacter, voici une adresse mail :
tienoundoubrand@gmail.com
Cordialement
Etienne Albrand
:Si quelqu'un voit le rapport avec Wikilivres, qu'il en fasse état. Dans le cas contraire je supprimerai ce message. [[Utilisateur:Sub|Sub]] 20 octobre 2011 à 19:53 (CEST)
== Importations depuis Wikipédia ==
Quand un contributeur juge qu'un paragraphe de Wikipédia (voire tout un article) correspond mieux à nos critères, il dépose [[w:Modèle:Pour Wikibooks]] sur celui-ci, et si c'est évident je le traite. Or, quand c'est moi qui juge que cela est nécessaire, plutôt que d'importer la page tout seul, et/ou coller ce modèle qui nous est destiné, je lance une discussion. Le problème est qu'elles ne sont pas classées, car le modèle ne catégorise pas les pages de discussions, et se confond [{{fullurl:w:Spécial:Pages liées|target=Mod%C3%A8le%3APour+Wikibooks&namespace=1}} avec les simple mentions du modèle] (via [[w:Modèle:m]]).
Du coup il faudrait dégager un consensus dans :
# [[w:Discussion:Liste des commandes MS-DOS]]
# [[w:Discussion:Fermeture (informatique)]]
[[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 22 octobre 2011 à 17:55 (CEST)
== Commission micro-financement de Wikimédia France ==
Bonjour à tous,
Depuis juillet, l'association [http://www.wikimedia.fr Wikimédia France] possède une « commission micro-financement ». Celle-ci est compétente pour attribuer des financements pour des projets rentrant dans les buts de l'association (soutenir la connaissance libre et les projets Wikimédia) et dont le budget est inférieur à 2000 euros - '''que ces projets viennent de personnes membres de l'association ou non'''. Ces projets peuvent être par exemple : l'organisation d'un événement local, l'achat de matériel photo pour un groupe de wikimédiens, un déplacement pour couvrir un événement, etc.
Si vous souhaitez faire financer un tel projet par l'association, n'hésitez pas à nous contacter ! La commission est joignable collectivement à l'adresse microfi (à) lists (point) wikimedia (point) fr. Un membre de la commission suivra votre projet dans toutes les étapes de son financement, de l'élaboration d'un budget prévisionnel à votre remboursement par l'association. Nous vous remercions cependant de nous contacter au moins deux semaines avant la date prévue de réalisation de votre projet.
Cordialement,
Pour la commission micro-financement de Wikimédia France, [[Utilisateur:Benjism89|Benjism89]] 1 novembre 2011 à 12:12 (CET)
== Du bon usage des catégories ==
Je suis au regret de constater que [[Utilisateur:JackPotte]] et moi-même nous menons une guerre d'édition larvée (cf historique de [[:Catégorie:Programmation Java (livre)]]). Le débat porte sur la pertinence de classement d'une catégorie « * (livre) » dans une catégorie thématique. Je vous copie ici l'échange que nous avons eu à ce sujet sur [[Discussion utilisateur:Sub|ma page de discussion]] :
<div style="margin: 10px; padding: 10px; border: solid 1px grey;">
Bonjour, je ne sais pas [{{fullurl:Catégorie:Programmation_en_Go_(livre)|diff=prev&oldid=334842}} de quelle convention tu parles], mais sur tous les sites Wikimédia (au moins francophones), quand on veut la liste des pages concernant un sujet (pour ajouter à sa liste de suivi en mode brut, préparer un bot...), on en dresse la liste en une seconde grâce aux sous-catégories. Par exemple pour [[:Catégorie:Java]], ce n'est plus possible depuis ce midi (contrairement à [[:en:Category:Java programming language]]). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 12 juin 2011 à 14:44 (CEST)
:En fait, j'essaie de maintenir la cohérence. La plupart des livres sont catégorisés de cette façon : page principale dans la catégorie thématique et catégorie du livre dans la [[:Catégorie:Livres par titre]]. Le problème, si on place les deux dans la catégorie thématique est que ça fait tout simplement doublon. Quand l'utilisateur cherche [[Patrons de conception]] dans [[:Catégorie:Génie logiciel]], il le voit directement classé parmi les livres de la catégorie et n'a pas besoin d'aller dans une sous-catégorie fouiller pour trouver où est la page de garde (c'est troublant de voir deux liens s'appeler pareil, pour le néophyte, la différence est trop subtile). Je conviens que ce système n'est pas parfait (notamment, d'après ce que tu décris), mais c'est pas moi qui l'ait choisi : c'est le résultat d'un compromis entre les pratiques courantes. Au début, j'étais opposé à la création des catégories ''(livre)'', parce que j'estime que ce n'est pas très utile (en tout cas pour les lecteurs, à la rigueur pour la maintenance...) et que ça complique le renommage des livres (je le pense toujours), d'autres pensent qu'elles sont utiles et qu'elles permettent notamment à chaque page de se trouver au moins dans une catégorie, afin de pouvoir traquer les pages sans catégories. À l'époque, je mettais un point d'honneur à ne pas ranger mes livres dans des catégories ''(livre)'' pour contester {{clin}}, aujourd'hui j'admets que la plupart de mes pairs les trouvent utiles donc j'accepte et je fais ce qu'on attend de moi. D'autres sont moins souple que moi (qui osera renommer [[:Catégorie:Photographie]] en [[:Catégorie:Photographie (livre)]] ?).
:Quant à placer des sous-pages dans les catégories thématiques, je n'y suis pas favorable. Notre projet propose des livres, les sous-pages sont des sous-éléments qui n'ont pas de cycle de vie propre mais sont un composant d'un tout : l'« unité » est le livre sur notre projet, c'est cela qu'on catégorise. En effet sur Wikipédia, l'« unité » est l'article, chaque article peut avoir ses catégories mais chaque article a son cycle de vie, dissocié des autres. Mettre des sous-pages d'un livre dans les catégories sur Wikibooks, ça reviendrait à essayer de classer les sections des articles de Wikipédia : ça n'a pas de sens selon moi. C'est mon avis, ce n'est pas celui de tout le monde. Aussi, j'avais proposé il y a quelques temps une solution qui aurait pu former un consensus, il s'agissait de permettre de classer aussi les sous-pages dans les catégories, dès lors qu'elles étaient séparées des pages principales (chaque catégories aurait été découpée en trois sections au lieu de deux : sous-catégories, livres et chapitres). Hélas, j'ai pris une [[Wikilivres:Prise de décision/Catégories|veste]] {{clin}} donc j'ai laissé tomber, je suis la pratique courante qui consiste 90% du temps à ne pas ranger les sous-pages. J'admets volontiers que tout ça n'est pas parfait, mais si tu souhaites relancer le débat sur le bon usage des catégories, tu auras mon soutient dans ta démarche, mais je crains que le sujet soit épineux. [[Utilisateur:Sub|Sub]] 12 juin 2011 à 15:24 (CEST)
</div>
Que dois-je faire ? Faut-il relancer le [[Wikilivres:Prise de décision/Catégories|débat sur les catégories]] ? [[Utilisateur:Sub|Sub]] 6 novembre 2011 à 12:18 (CET)
:Je propose tout d'abord que l'on applique un principe fondamental de la programmation à ce problème énorme : le découper en petites questions à débattre plutôt que débattre sur des questions différentes ou l'on risque de se perdre et se disperser à parler de choses différentes :
:* Classement des sous-pages de livres dans la catégorie ''titre (livre)'' : oui ou non ?
:* Classement de la page principale dans la catégorie ''titre (livre)'' : oui ou non ?
:* Classement de la catégorie ''titre (livre)'' dans la catégorie ''Livres par titre'' : oui ou non ?
:* Classement de la catégorie ''titre (livre)'' dans la catégorie thématique correspondante : oui ou non ?
:* Classement de la page principale ''titre'' dans la catégorie thématique correspondante : oui ou non ?
:* Classement des sous-pages de livres dans la catégorie thématique correspondante : oui ou non ?
:* ''...autres débats auxquels je n'ai pas pensé ci-dessus...''
:Donc il faut créer des pages de débat séparées afin de répondre très précisément aux questions et que tout soit clair pour tout le monde.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 6 novembre 2011 à 12:45 (CET)
::Je crains qu'on ne puisse ici appliquer le principe que tu préconises et qu'on doit plus à Descartes ([[q:René Descartes#Discours de la méthode, 1637|Discours de la méthode]] : « Le second [principe] était de diviser chacune des difficultés que j'examinais en autant de parcelles qu'il se pourrait et qu'il serait requis pour mieux les résoudre [...] ») qu'à la programmation {{clin}}. En effet, il me semble que les réponses à chacune de ces questions sont liées, par exemple parce que certains souhaitent que toute page se trouve au moins dans une catégorie (ce qui exclue plusieurs combinaisons de « non »). [[Utilisateur:Sub|Sub]] 6 novembre 2011 à 15:07 (CET)
:::J'ai peut-être trop détaillé, mais autant regrouper les débats :
:::* (page de débat) Quelle catégorie pour les sous-pages d'un livre ?
:::** (question) Classement des sous-pages de livres dans la catégorie ''titre (livre)'' : oui ou non ?
:::** (question) Classement des sous-pages de livres dans la catégorie thématique correspondante : oui ou non ?
:::** ...autres propositions...
:::* (page de débat) Quelle catégorie pour la page principale d'un livre ?
:::** (question) Classement de la page principale dans la catégorie ''titre (livre)'' : oui ou non ?
:::** (question) Classement de la page principale ''titre'' dans la catégorie thématique correspondante : oui ou non ?
:::** ...autres propositions...
:::* (page de débat) Quelle catégorie pour la catégorie ''titre (livre)'' d'un livre ?
:::** (question) Classement de la catégorie ''titre (livre)'' dans la catégorie ''Livres par titre'' : oui ou non ?
:::** (question) Classement de la catégorie ''titre (livre)'' dans la catégorie thématique correspondante : oui ou non ?
:::** ...autres propositions...
:::* ...autres débats sur les catégories...
:::Et la réponse n'est pas forcément oui ou non, mais débattre le oui et débattre le non.
:::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 6 novembre 2011 à 15:15 (CET)
::::En plus de l'aspect pratique, je tiens quant-à-moi à exposer pourquoi l'ergonomie des sous-pages dans la même catégorie, qui a le monopole sur tous les autres projets que j'ai vu, parait la plus naturelle aux contributeurs Wikimédia (sauf Wikisource qui inclut le contenu de ses .djvu dans une page catégorisée) :
::::# [[:Catégorie:Archives]].
::::# [[w:Catégorie:Wikipédia:Bot/Requêtes]].
::::# [[wikt:Catégorie:Wikidémie]].
::::# [[v:Catégorie:Java]].
::::# [[n:Catégorie:Prise de décision]].
::::# [[:en:Category:Eclipse]].
::::# [[w:en:Category:Approved Wikipedia bot requests for approval]].
::::[[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 6 novembre 2011 à 15:40 (CET)
:::::Pour moi les points 1, 2, 3, 5, 7 ne peuvent servir de point de comparaison étant donné qu'il s'agit de catégories administratives et de la classification du contenu du wiki en lui-même or, il me semble qu'il y a un consensus sur le fait qu'il faut bien distinguer le rangement administratif et le rangement thématique. En revanche, force est d'admettre que la catégorisation des sous-pages dans la catégorie thématique est effectivement la pratique (plus ou moins généralisée) sur v: et en:b: comme dans les deux exemples que JackPotte donne. [[Utilisateur:Sub|Sub]] 6 novembre 2011 à 18:20 (CET)
::::::Concernant Wikibooks en anglais, il faut signaler qu'ils ont un espace de nom supplémentaire Subject (espace de nom n° 112) leur permettant de catégoriser les livres traitant d'un même sujet.
::::::Exemple [[:en:Subject:Java_programming_language]]
::::::En résumé :
::::::* catégorie administrative -> Category: sur en:.
::::::* catégorie thématique -> Subject: sur en:.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 26 novembre 2011 à 14:28 (CET)
:::::::Je constate que cet espace ne contient que certaines thématiques parentes, complémentaires de [[:en:Category:Java programming language]]. Un peu comme les départements de la Wikiversité. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 26 novembre 2011 à 23:26 (CET)
::::::::Ils ont aussi des [[:en:Category:Allbooks categories|catégories ''*/all books'']]. Je ne comprends pas très bien comment tout ça fonctionne. [[Utilisateur:Sub|Sub]] 27 novembre 2011 à 15:31 (CET)
== Problème du rendu PDF des collections ==
Bonjour,
Certains rendus de PDF avec les collections diffèrent largement du rendu HTML du navigateur, et notamment, la disposition des éléments n'est pas respectée.
Voir les problèmes de [[S'initier au boulier en 10 leçons]] ([[Discussion:S'initier au boulier en 10 leçons]] et [[Aide:Livres/Problèmes]]).
Pour chaque collection, il faut donc vérifier et modifier plusieurs pages afin de trouver comment le rendu PDF peut être amélioré (souvent sans jamais parvenir à la présentation parfaite).
J'ai signalé le problème : [[bugzilla:32212]]. Cependant je ne suis pas sûr qu'il soit corrigé à moins que ce bug obtienne plus de votes que le mien seul.
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 7 novembre 2011 à 14:44 (CET)
:{{fait}} As-tu demandé du renfort sur le projet anglophone ? [[Utilisateur:Perditax|Perditax]] 8 décembre 2011 à 10:45 (CET)
::{{Fait}} [[:en:Wikibooks:Reading_room/General#PDF_rendering_problems]] -- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 8 décembre 2011 à 15:31 (CET)
== Doublons avec la Wikiversité ==
Peut-être qu'une liste exhaustive des doublons ne suffit pas pour décider de les fusionner ou les répartir selon des critères clairs, mais elle avance : [[v:Wikiversité:La salle café/47 2011]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 25 novembre 2011 à 20:18 (CET)
== Tutoriel pour le focus stacking ==
Bonsoir à tous.
Il y a quelque temps, j'avais prévu de faire cela. C'est chose faite [[Photographie/Netteté_des_images/Profondeur_de_champ/Considérations_pratiques#Avec_CombineZP_:|ici]]. --[[Utilisateur:ComputerHotline|ComputerHotline]] 2 décembre 2011 à 22:17 (CET)
:Merci, ça pourrait être mis dans {{modl|nouveau livre}} si on considère qu'il s'agit d'un tome supplémentaire. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 3 décembre 2011 à 12:28 (CET)
== Modèle surligner et rendu pdf ==
{{Bug|32212}}
Salut,
je viens de voir dans [[Cristallographie géométrique/Réseaux de Bravais]] que le modèle {{m|surligner}} ne s'affiche pas correctement dans la version pdf : [{{surligner|1}}11] (notation de direction en cristallo) donne [111] dans le pdf, alors que ce sont deux directions différentes. C'est assez gênant parce que je vais vraiment avoir besoin du modèle. Remplacer <nowiki>{{surligner|1}}</nowiki> par <nowiki><span style="text-decoration:overline">1</span></nowiki> n'aide pas non plus. Est-ce qu'il y a un moyen de corriger ça ? (J'ai essayé avec [[:w:Indices de Miller et indices de direction]] et c'est pareil.) [[Utilisateur:Perditax|Perditax]] 8 décembre 2011 à 01:12 (CET)
:Il y a plusieurs problèmes de rendus PDF mais qui ne seront pas corrigés si personne ne vote pour ce bug : [[bugzilla:32212]]
:Et comme ceci [<span style="border-top:solid 1px black;">1</span>11]
:<code><nowiki>[<span style="border-top:solid 1px black;">1</span>11]</nowiki></code>
:Ça ne fonctionne pas mieux.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion_Utilisateur:DavidL|discuter]] ► 8 décembre 2011 à 02:43 (CET)
::En attendant, j'ai créé [[Modèle:Avertissement PDF|ce modèle]] (non imprimable) pour avertir des éventuels problèmes de compilation, à poser sur les pages concernées. En fouillant dans les modèles, j'ai trouvé {{m|Version LaTeX}} qui n'a pas l'air d'être utilisé, est-ce qu'on pourrait envisager une alternative en proposant une traduction wiki → LaTeX automatique ? Je sais, pas facile, je ne serais pas capable de le programmer correctement et je ne parle que le fortran. [[Utilisateur:Perditax|Perditax]] 11 décembre 2011 à 10:46 (CET)
== Nouveau bug ==
{{Bug|33165}}
Je ne peux plus supprimer de page depuis aujourd'hui. Pour l'instant j'ai tenté avec :
# [[Ma première histoire de Belgique]]
# [[Utilisateur:JackPotte/Méta-bandeau]]
Quelle que soient les justifications sélectionnées. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 15 décembre 2011 à 00:03 (CET)
:J'ai réussi à supprimer [[Ma première histoire de Belgique]].
:Peut-être qu'il s'agissait d'un problème temporaire. Il arrive parfois que des messages d'erreurs s'affichent durant certaines opérations quand la base de données est verrouillée.
:Tu peux essayer de restaurer la page, puis la supprimer à nouveau.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 15 décembre 2011 à 01:01 (CET)
::'''Bug [[bugzilla:33165]]'''
::Maintenant j'ai une erreur lors de la suppression :
::[[Fichier:MediaWiki 1 18 error getText.png]]
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 15 décembre 2011 à 12:59 (CET)
:::Cette erreur est réparée pour moi à présent. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 15 décembre 2011 à 19:23 (CET)
::::Effectivement, j'ai pu supprimer des pages.
::::N'hésite pas à réouvrir le rapport de bug existant si ça se reproduit.
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 15 décembre 2011 à 20:09 (CET)
== Modifier l'en-tête ==
Salut, dans mes préférences j'ai coché la case OngletEntête pour ne modifier que le texte d'introduction des pages, mais je vois que le gadget ne marche pas, en cliquant sur l'onglet je tombe sur des pages comme http://fr.wikibooks.org/w/undefined§ion=0 (→ erreur 404 de la wikimedia foundation) ou http://fr.wikibooks.org/wiki/Undefined%26section%3D0 (modification de page inexistante sur wikibooks). Ce n'est pas la première fois que je m'en rends compte, je pensais au début que c'était parce que je voulais modifier une sous-page de livre, mais ça m'arrive aussi avec des pages principales comme [[Randonnée dans les Alpes]]. Merci à la personne qui saura corriger le problème. [[Utilisateur:Perditax|Perditax]] 16 décembre 2011 à 10:06 (CET)
:Salut,
:J'ai testé, mais cela semble fonctionner sous Firefox 8.0 et IE 9 (les 2 sous Windows 7).
:Quelques questions sur le problème :
:#Quels navigateur et OS utilises-tu ?
:#Quelle adresse vois-tu quand tu passe la souris sur l'onglet "en-tête" avant de cliquer dessus ?
:#Quelle adresse y-a t-il dans la barre d'adresse du navigateur une fois que tu as cliqué ?
:#Quelle est la liste des gadgets que tu as coché ? (il y a peut-être un autre gadget qui perturbe le fonctionnement de l'onglet)
:
:Pour répondre rapidement aux questions 1 et 4, tu peux utiliser le gadget DebugTools :
:#Coche le gadget "{{MediaWiki:Gadget-DevTools}}" et sauvegarde les préférences.
:#Clique le lien 'outils de script' qui doit apparaitre après le lien 'déconnexion'.
:#Entre seulement un point d'interrogation (?) dans la zone de gauche puis clique le bouton "Exécuter".
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 décembre 2011 à 11:52 (CET)
::J'ai modifié le gadget. Il continue de fonctionner sous Firefox 8.0 et IE 9 (les 2 sous Windows 7).
::Si tu as toujours le problème, les réponses aux questions ci-dessus me serait utiles.
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 décembre 2011 à 14:50 (CET)
:::Je viens juste de rentrer du boulot. Apparemment ça ne fonctionne toujours pas pour moi.
:::#Firefox 8.0.1 et Windows XP SP3
:::#Essai sur [[Randonnée dans les Alpes]] : http://fr.wikibooks.org/wiki/null§ion=0 quand je passe la souris dessus
:::#http://fr.wikibooks.org/wiki/Null%26section%3D0 dans le champ du navigateur
:::#Gadgets activés dans les préférences : HistoryNumDiff, DeluxeHistory, SousPages, OngletEntête, OngletPurger, OngletEditcount, OngletGoogle, Barre de luxe, LongEditSummaries, Hot Cats, Popups, FastRevert
:::Pour DebugTools, le résultat est dans [[Utilisateur:Perditax/MaConfiguration]]. [[Utilisateur:Perditax|Perditax]] 16 décembre 2011 à 18:05 (CET)
::::J'ai testé avec les mêmes gadgets activés : toujours pas de problème visible.
::::As-tu vidé le cache du navigateur ou forcé le rechargement (Ctrl+Shift+R sous Firefox) ?
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 décembre 2011 à 18:28 (CET)
:::::Je viens de vider le cache, c'est pareil. Sur wp ça marche sans problème... la différence étant que j'y suis sous monobook. Je viens de changer de vector à monobook et maintenant c'est bon. Merci. [[Utilisateur:Perditax|Perditax]] 16 décembre 2011 à 18:51 (CET)
::::::J'ai corrigé le gadget pour le rendre compatible avec vector et monobook.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 décembre 2011 à 19:27 (CET)
== Joyeux Noël ==
{|
|-----
| [[File:Merry-christmas.gif]]
| [[File:Christmas decorations on a tree - closeup.JPG|300px]]
|-----
| colspan="2" style="text-align:center;font-size:32pt;font-weight:bold;color:blue;line-height:32pt;" | Joyeux Noël à tous !
|}
-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 24 décembre 2011 à 18:28 (CET)
:MERCI !!! Joyeux Noël à toi aussi ! [[Utilisateur:TouzaxA|TouzaxA]], [[Discussion utilisateur:TouzaxA|Discuter]] Le 26 décembre 2011 à 09:25 (CET)
::Meilleurs vœux ! [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 26 décembre 2011 à 21:06 (CET)
:::Merci, joyeux Noël et très bonnes fêtes {{bisou}} [[Utilisateur:Perditax|Perditax]] 27 décembre 2011 à 08:20 (CET)
jlusj1bi1y523w4xdib9aop9x5nmll2
Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10
0
67393
763106
763102
2026-04-07T12:18:30Z
Mewtow
31375
/* L’implémentation matérielle du pipeline géométrique */
763106
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, pour les injecter dans le reste du pipeline.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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.
===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}}
d3iks6ncbb8l94rpntmox8nigu5mqp7
763107
763106
2026-04-07T12:40:48Z
Mewtow
31375
/* L’implémentation matérielle du pipeline géométrique */
763107
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'''input assembler''===
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, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de deux circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Les unités ''index fetch'' et ''vertex fetch'' font juste des calculs d'adresse et des accès mémoire. Le circuit d'''index fetch'' balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===L'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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.
===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}}
82al7nn47kahnp15n7nkori40nn5tt6
763108
763107
2026-04-07T12:41:18Z
Mewtow
31375
/* Les cartes accélératrices des PC grand publics : les années 90-2000 */
763108
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'''input assembler''===
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, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de deux circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Les unités ''index fetch'' et ''vertex fetch'' font juste des calculs d'adresse et des accès mémoire. Le circuit d'''index fetch'' balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
kklkwfq5ch0uvjpwdjbdkxt6tam6u5d
763109
763108
2026-04-07T12:45:05Z
Mewtow
31375
/* L'input assembler */
763109
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'''input assembler''===
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, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de deux circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
dmuwjh85w0suenqplkbcjxzcvrt7a45
763110
763109
2026-04-07T12:45:46Z
Mewtow
31375
/* L'input assembler */
763110
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de deux circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
guflxymkhcdhy0zurqti5qsd45zszz9
763111
763110
2026-04-07T12:52:41Z
Mewtow
31375
/* L'input assembler */
763111
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
169878m1jj7u3vougtfiqn5glnhfse5
763112
763111
2026-04-07T13:08:39Z
Mewtow
31375
/* Les caches de sommets */
763112
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l'''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch''', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. En clair, une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l'''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l'''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elles est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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''.]]
Pour résumer, l'''input assembler'' contient 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 ).
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
345v8nevshvgwgjfap9zep44cdpxrpc
763113
763112
2026-04-07T13:09:20Z
Mewtow
31375
/* Les caches de sommets */
763113
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch''', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. En clair, une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l'''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l'''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elles est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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''.]]
Pour résumer, l’''input assembler'' contient 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 ).
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
aepp6n5yudnlikge2k8jtuaauo994i6
763114
763113
2026-04-07T13:20:54Z
Mewtow
31375
/* Les caches de sommets */
763114
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch''', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elles est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient 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 ).
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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
ixfexuuwthmjvscz1fyqor23pzul2g8
763115
763114
2026-04-07T13:23:52Z
Mewtow
31375
/* Les caches de sommets */
763115
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch''', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, mais de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elles est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
qmty3otvd9ub07aard8vmetwc5f5egi
763116
763115
2026-04-07T13:24:53Z
Mewtow
31375
/* Les caches de sommets */
763116
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch''', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
qp41u0q02qjqg9t9sy3cna8wq0rp0pn
763117
763116
2026-04-07T13:26:08Z
Mewtow
31375
/* Les caches de sommets */
763117
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' est une mémoire cache, qui mémorise les N derniers sommets calculés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. 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.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
27sn94xyscdglkige2ecipxg4aymitq
763118
763117
2026-04-07T13:28:40Z
Mewtow
31375
/* Les caches de sommets */
763118
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'''input assembler''===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
j1kvw38napjyb8j3xh3e4ogwthld4st
763119
763118
2026-04-07T13:29:28Z
Mewtow
31375
/* L'input assembler */
763119
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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
tlvnymajofk1r2pbjpfie872tipd0j0
763120
763119
2026-04-07T13:29:42Z
Mewtow
31375
/* Les caches de sommets */
763120
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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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''.]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
92lg6l1rcv65lijin4o29zbmn1z4q4p
763121
763120
2026-04-07T13:37:38Z
Mewtow
31375
/* Les caches de sommets : une optimisation du tampon d'indice */
763121
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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
f7f70rvzja7qhvgi5y7npfzyo8ocu6j
763124
763121
2026-04-07T13:50:23Z
Mewtow
31375
/* Les caches de sommets : une optimisation du tampon d'indice */
763124
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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Il est appelé le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
kfom815h1rmmg0pl84wxcgqjy2kpmmx
763125
763124
2026-04-07T13:51:53Z
Mewtow
31375
/* Les caches de sommets : une optimisation du tampon d'indice */
763125
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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
hr9r9305jp3mp8zfz693kvh0su7cse8
763128
763125
2026-04-07T15:35:48Z
Mewtow
31375
763128
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 étapes suivantes :
* L'étape de '''transformation''' effectue des 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 dit si le sommet est fortement éclairé ou 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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités ''index fetch'' et ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
3xh7ufh1zoie1hbhlhl147yyv1tc345
763129
763128
2026-04-07T15:44:42Z
Mewtow
31375
/* L'input assembler et le tampon d'indice */
763129
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 étapes suivantes :
* L'étape de '''transformation''' effectue des 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 dit si le sommet est fortement éclairé ou 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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice.
Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' a commencé à être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
igx18lpguzithh44tdk7clugdt1a97e
763130
763129
2026-04-07T15:51:46Z
Mewtow
31375
/* L'input assembler et le tampon d'indice */
763130
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 étapes suivantes :
* L'étape de '''transformation''' effectue des 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 dit si le sommet est fortement éclairé ou 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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice.
Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
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]]
Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
nfthwply4cjm9whq1gpty1eicar6qp9
763206
763130
2026-04-07T18:10:03Z
Mewtow
31375
/* L'assemblage de primitives */
763206
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 étapes suivantes :
* L'étape de '''transformation''' effectue des 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 dit si le sommet est fortement éclairé ou 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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice.
Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. 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.
Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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'é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}}
dom6vrfvbaqdc3tnlxocghqj4ez2wo3
763207
763206
2026-04-07T18:13:25Z
Mewtow
31375
/* L'assemblage de primitives */
763207
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 étapes suivantes :
* L'étape de '''transformation''' effectue des 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 dit si le sommet est fortement éclairé ou 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'''input assembler'' et le tampon d'indice===
Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline.
[[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]]
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.
Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressé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.
Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques.
[[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]]
Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela.
Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice.
Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances.
Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice.
Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''.
===Les caches de sommets : une optimisation du tampon d'indice===
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 quand des sommets sont dupliqués, ce n'est pas le cas. 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.
Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants.
L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''.
Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''.
Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide.
Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite.
[[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]]
Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et 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, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et 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. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances.
[[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]]
Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. 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'assemblage de primitives===
En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. 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.
Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées.
Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle.
Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles.
Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. 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. 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'é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}}
qyv9tbu3g9qw2cszw3e6wkptti9daw3
Programmation PHP avec Symfony/Doctrine
0
71848
763126
754132
2026-04-07T14:48:00Z
JackPotte
5426
/* Repository */
763126
wikitext
text/x-wiki
<noinclude>{{PHP}}</noinclude>
== Installation ==
{{w|Doctrine (ORM)|Doctrine}} est l'ORM par défaut de Symfony. Il utilise {{w|PHP Data Objects|PDO}}. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la [[Patrons de conception/Chaîne de responsabilité|chaîne de responsabilité]].
Installation en SF4<ref>https://symfony.com/doc/current/doctrine.html</ref> :
<pre>
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
</pre>
Renseigner l'accès au SGBD dans le .env :
<pre>
DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
</pre>
Ensuite la base de données doit être créée avec :
<pre>
php bin/console doctrine:database:create
</pre>Si la commande précédente échoue avec le message d'erreur suivant:
''Could not create database "database_name" for connection named default''
''An exception occurred in the driver: could not find driver''
Ce qui veut dire que vous devez installer le driver approprié.
'''Exemple:'''
sudo apt install php8.3-pgsql{{remarque|1=symfony/orm-pack équivaut aux paquets suivants, qui peuvent bien sûr être installés séparément à la place :
*doctrine/doctrine-bundle
*doctrine/doctrine-migrations-bundle
*doctrine/orm
*symfony/proxy-manager-bridge
}}
== Commandes Doctrine ==
Exemples de commandes :
<pre>
php bin/console doctrine:query:sql "SELECT * FROM ma_table"
php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
# Ces deux commandes sont équivalentes des précédentes
php bin/console dbal:run-sql "SELECT * FROM ma_table"
php bin/console dbal:run-sql "$(< mon_fichier.sql)"
php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:cache:clear-query
php bin/console doctrine:cache:clear-result
</pre>
== Entity ==
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs {{wt|getter}}s et {{wt|setter}}s respectifs. Pour en générer une :
<pre>
php bin/console generate:doctrine:entity
</pre>
Cette association est définie par des attributs Doctrine. Pour les vérifier :
<pre>
php bin/console doctrine:schema:validate
</pre>
=== Exemple ===
Voici par exemple plusieurs types d'attributs :
<syntaxhighlight lang=php>
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
</syntaxhighlight>
{{(|Exemple avec annotation (avant PHP 8)}}
<syntaxhighlight lang=php>
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="spelling", type="string", length=255, nullable=false)
*/
private $spelling;
/**
* @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
*/
private $pronunciation;
/**
* @var Language
*
* @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
* @ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
*/
private $homophons;
</syntaxhighlight>
{{)}}
Et leurs modificateurs (getters et setters) :
<syntaxhighlight lang=php>
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
</syntaxhighlight>
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec <code>$word->setLanguage($language)</code>.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi un deuxième attribut <code>#[ORM\JoinColumn</code> (anciennement <code>@ORM\JoinColumn</code>) mentionnant la clé étrangère en base de données (et pas en PHP).
=== Bonnes pratiques ===
L'attribut <code>#[ORM\Table(name: 'word')]</code> était facultatif dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.
Avant PHP 8, les contraintes d'unicité (utiles entre autres pour les clés composites) étaient encapsulées dans l'annotation <code>Table</code>, mais ce n'est plus le cas avec les attributs :
<pre>
#[ORM\UniqueConstraint(name: 'spelling-pronunciation', columns: ['spelling', 'pronunciation'])]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
</pre>
{{)}}
{{attention|Dans les relations *toMany :
* il faut initialiser l'attribut dans le constructeur en <code>ArrayCollection()</code>.
* on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
* cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
}}
{{attention|Il faut ajouter le <code>#[ORM\JoinColumn(</code> dans les deux entités liées, car :
* dans aucune cela renvoie <code>Could not resolve type of column "id"</code>
* dans une seule cela provoque un problème N+1 (celle qui ne l'a pas appelle celle qui l'a pour chacun de ses enregistrements, même si celle qui l'a n'est pas utilisée ensuite).
}}
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec <code>length=0</code><ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string</ref>. Le type "text" par contre n'a pas de limite.
=== ArrayCollection ===
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
* Dans une entité : <code>#[ORM\OrderBy(['sort_order' => 'ASC'])]</code> (anciennement <code>@ORM\OrderBy({"sort_order" = "ASC"})</code>).
* Sinon, instancier un critère :
<pre>
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
</pre>
=== GeneratedValue ===
L'annotation ''GeneratedValue'' peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
{{attention|
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID<ref>https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations</ref>. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur '' Integrity constraint violation'' puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
<syntaxhighlight lang=php>
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
</syntaxhighlight>
}}
=== Triggers ===
Les opérations en cascade sont définies sous deux formes d'attributs :
* <code>#[ORM\OneToMany(cascade: ['persist', 'remove'])]</code> : au niveau ORM.
* <code>#[ORM\JoinColumn(onDelete: 'CASCADE')]</code> : au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
=== Concepts avancés ===
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware<ref>https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html</ref>.
{{attention|Les types des attributs peuvent être quelque peu différents du SGBD<ref>https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix</ref>.}}
{{attention|Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").}}
L'autojointure est appelé ''self-referencing association mapping'' par Doctrine<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing</ref>).
=== Héritage ===
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html</ref> :
<syntaxhighlight lang=php>
/** @MappedSuperclass */
class MyEntityParent
...
</syntaxhighlight>
=== Tables sans classe ===
Doctrine peut créer des tables de mapping sans entité, si on précise son nom dans les deux tables reliées :
<pre>
#[ORM\JoinTable(name: 'table_de_mapping')]
#[ORM\JoinColumn(name: 'table_source_id', referencedColumnName: 'table_source_id')]
#[ORM\InverseJoinColumn(name: 'table_cible_id', referencedColumnName: 'table_cible_id')]
#[ORM\ManyToMany(targetEntity: TableCible::class)]
</pre>
== EntityManager ==
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données, définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
* persist() : prépare un INSERT SQL (rattache une entité à un entity manager).
* remove() : prépare un DELETE SQL.
* flush() : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
* merge() : fusionne une entité absent de l'em dedans.
* refresh() : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger ''after insert'' sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par <code>$createdDate</code> :
<syntaxhighlight lang=php>
$entity = new MyEntity();
$em->persist($entity);
$em->flush($entity);
// Trigger SGBD déclenché ici en parallèle
$em->refresh($entity);
$entity->setCreatedDate($createdDate);
$em->flush($entity);
</syntaxhighlight>
== Repository ==
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de <code>Doctrine\ORM\EntityRepository</code>. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés ''NomDeLEntitéRepository''.
{{remarque|D'un point de vue architectural, avant d'instancier une nouvelle entité, on utilise généralement le repository pour savoir si son enregistrement existe en base ou si on doit le créer. Dans ce deuxième cas, la bonne pratique en {{wt|DDD}} est d'utiliser une Factory pour faire le new de l'entité, mais aussi pour les new de son agrégat si elle est le nœud racine. Par exemple une <code>CarFactory</code> fera un <code>new Car()</code> mais aussi créera et lui associera ses composants : <code>new Motor()</code>...}}
{{remarque|Il est possible de préciser le nom du repository d'une entité dans cette dernière :
<pre>
#[ORM\Entity(repositoryClass: \App\Repository\WordRepository::class)]
</pre>
{{(|Exemple avec annotation (avant PHP 8)}}
<pre>
@ORM\Entity(repositoryClass="App\Repository\WordRepository")
</pre>
{{)}}
}}
=== SQL ===
==== Depuis Doctrine ====
Utile pour exploiter les fonctionnalités du SGBD utilisé, absentes de Doctrine.
Par exemple, pour appeler une procédure stockée :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
</pre>
Ou tronquer une table :
<pre>
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('TRUNCATE TABLE ma_table', $rsm)->getResult();
</pre>
{{remarque|Dans cet exemple, on peut aussi utiliser ceci :
<pre>
$connection = $this->_em->getConnection();
$databasePlatform = $connection->getDatabasePlatform();
$connection->executeStatement(
$databasePlatform->getTruncateTableSQL($this->_em->getClassMetadata(MonEntite::class)->getTableName(), true),
);
</pre>
}}
==== Sans Doctrine ====
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :
<pre>
return $this->connection->fetchAll($sql);
</pre>
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut ''entity manager'' (''em'', ou ''_em'' pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
<syntaxhighlight lang=php>
use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
</syntaxhighlight>
2. En injectant le service de connexion dans le constructeur (<code>'@database_connection'</code>) :
<pre>
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
</pre>
=== DQL ===
==== Méthodes magiques ====
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
* <code>$repo->find($id)</code> : cherche par la clé primaire définie dans l'entité.
* <code>$repo->findAll()</code> : récupère tous les enregistrements (sans clause <code>WHERE</code>).
* <code>$repo->findById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id</code> dans la table associée au repo.
* <code>$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])</code> engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname</code>.
* <code>$repo->findOneById($id)</code> : engendre automatiquement un <code>SELECT * WHERE id = $id LIMIT 1</code>.
* <code>$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])</code> : engendre automatiquement un <code>SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1</code>.
{{attention|Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "<code>findById</code>" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser <code>findBy()</code>.
}}
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
<syntaxhighlight lang=php>
$repo->findBy(
['lastname' => $lastname], // where
['lastname' => 'ASC'], // order by
10, // limit
0, // offset
);
</syntaxhighlight>
==== createQuery ====
DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec <code>IDENTITY()</code> pour les jointures. Ex :
<pre>
public function findComplicatedStuff()
{
$em = $this->getEntityManager();
$query = $em->createQuery("
SELECT
u.last_name, u.first_name
FROM
App\Entity\Users u
INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users)
WHERE
i.status='waiting'
");
return $query->getResult();
}
</pre>
==== createQueryBuilder ====
L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel <code>createQueryBuilder()</code> :
<syntaxhighlight lang=php>
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
</syntaxhighlight>
Pour éviter le <code>SELECT *</code> dans cet exemple, on peut y ajouter la méthode <code>->select()</code>.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
===== Jointures =====
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
<pre>
use Doctrine\ORM\Query\Expr\Join;
...
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
</pre>
Pour filtrer quand une jointure toMany contient des résultats, utiliser <code>EMPTY</code> :
<pre>
...
->andWhere('files IS NOT EMPTY')
</pre>
===== Résultats =====
Doctrine peut renvoyer avec :
* <code>getResult()</code> : un objet ArrayCollection (iterable, pour rechercher dedans : <code>->contains()</code>), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;
* <code>getArrayResult()</code> ou <code>getScalarResult()</code> : un tableau de tableaux (entité normalisée) ;
* <code>getSingleColumnResult()</code> : un tableau unidimensionnel.
On peut aussi utiliser <code>->indexBy('id')</code> pour définir que les clés du tableau de résultat soir les IDs de l'entité.
===== Cache =====
====== Configuration globale ======
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
<syntaxhighlight lang=yaml>
framework:
cache:
default_redis_provider: '%env(REDIS_URL)%'
pools:
doctrine.metadata_cache_pool:
adapter: cache.system
doctrine.query_cache_pool:
adapter: cache.system
doctrine.result_cache_pool:
adapter: cache.app
</syntaxhighlight>
Puis dans doctrine.yaml, les utiliser :
<syntaxhighlight lang=yaml>
doctrine:
orm:
metadata_cache_driver:
type: pool
pool: doctrine.metadata_cache_pool
query_cache_driver:
type: pool
pool: doctrine.query_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
</syntaxhighlight>
À partir de là le cache des métadonnées est utilisé partout.
====== Configuration par entité ======
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
* Dans l'entité, avec un attribut <code>#[ORM\Cache(usage: 'READ_ONLY', region: 'write_rare')]</code> (anciennement <code>@ORM\Cache(usage="READ_ONLY", region="write_rare")</code><ref>https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6</ref>), utilisant la configuration doctrine.yaml :
<pre>
doctrine:
orm:
second_level_cache:
enabled: true
regions:
write_rare:
lifetime: 864000
cache_driver: { type: service, id: cache.app }
</pre>
* Dans le repository :
<syntaxhighlight lang=php>
$query
->useQueryCache($hasQueryCache)
->setQueryCacheLifetime($lifetime)
->enableResultCache($lifetime)
;
</syntaxhighlight>
Dans cet exemple, on n'utilise pas <code>cache.system</code> pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. <code>cache.app</code> pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Pour invalider le cache d'une entité afin que les findAll() renvoient la liste à jour depuis la base de données modifiée :
<pre>
$em->getCache()->evictEntityRegion(myEntity::class);
</pre>
==== Expressions ====
Pour ajouter une expression en DQL, utilise <code>$qb->expr()</code>. Ex<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class</ref> :
* <code>$qb->expr()->count('u.id')</code>
* <code>$qb->expr()->between('u.id', 2, 10)</code> (entre 2 et 10)
* <code>$qb->expr()->gte('u.id', 2)</code> (plus grand ou égal à 2)
* <code>$qb->expr()->like('u.name', '%son')</code>
* <code>$qb->expr()->lower('u.name')</code>
* <code>$qb->expr()->substring('u.name', 0, 1)</code>
==== Injection de dépendances ====
Les repository DQL deoivent ''ServiceEntityRepository'' :
<syntaxhighlight lang=php>
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Word::class);
}
}
</syntaxhighlight>
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
* Étendre une classe qui étend ''ServiceEntityRepository''.
* Le redéfinir dans services.yaml.
* Utiliser un trait.
== Transactions ==
Pour garantir d'intégrité d'une transaction<ref>https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly</ref> :
<syntaxhighlight lang=php>
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$this->persist($myEntity);
$this->flush();
$connection->commit();
} catch (Exception $e) {
$connection->rollBack();
throw $e;
}
</syntaxhighlight>
Il existe aussi une syntaxe alternative :
<syntaxhighlight lang=php>
$em->transactional(function($em, $myEntity) {
$em->persist($myEntity);
});
</syntaxhighlight>
== Évènements ==
Pour ajouter des triggers sur la mise à jour d'une table, il y a deux solutions :
* ajouter dans son entité l'attribut <code>#[ORM\HasLifecycleCallbacks]</code>, et à ses méthodes l'attribut de l'évènement concerné. Ex :
<pre>
#[ORM\PrePersist]
public function setCreatedAt(): self
{
$this->createdAt = new DateTime();
return $this;
}
</pre>
* ajouter des tags dans services.yaml. Ex :
<pre>
App\EventListener\MyEntityListener:
tags:
- { name: doctrine.event_listener, event: PrePersist }
</pre>
Voici les [[Programmation PHP avec Symfony/Évènement|évènements]] utilisables ensuite (dans les listeners / subscribers) :
=== prePersist ===
Se produit avant la persistance d'une entité (paramètre : <code>PrePersistEventArgs $args</code>).
=== postPersist ===
Se produit après la persistance d'une entité (<code>PostPersistEventArgs $args</code>).
=== preUpdate ===
Se produit avant l'update d'une entité (<code>PreUpdateEventArgs $args</code>).
=== postUpdate ===
Se produit après l'update d'une entité (<code>PostUpdateEventArgs $args</code>).
=== preRemove ===
Se produit avant l'update d'une entité (<code>PreRemoveEventArgs $args</code>).
=== postRemove ===
Se produit après l'update d'une entité (<code>PostRemoveEventArgs $args</code>).
=== preFlush ===
Se produit avant la sauvegarde d'une entité (<code>PreFlushEventArgs $args</code>).
{{attention|
* Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
* Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
* Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler <code>$unitOfWork->computeChangeSet($classMetadata, $entity)</code><ref>https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener</ref>.
}}
{{remarque|On peut aussi appeler computeChangeSet() depuis ailleurs pour savoir si une entité va occasionner une requête SQL lors de son flush<ref>https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2</ref>.}} Ex :
<pre>
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
if ($uow->isEntityScheduled($myEntity)) {
//...
}
</pre>
{{remarque|On peut aussi utiliser le paramètre <code>LifecycleEventArgs $args</code> dans ces fonctions.}}
{{attention|Parfois le <code>$object::class</code> peut renvoyer <code>Proxies\__CG__\App\Entity\MyEntity</code> au lieu de <code>App\Entity\MyEntity</code>, selon le cache utilisé.}}
=== postFlush ===
Se produit après la sauvegarde d'une entité (<code>PostFlushEventArgs $args</code>).
== Migrations ==
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
<syntaxhighlight lang=bash>
composer require doctrine/doctrine-migrations-bundle
</syntaxhighlight>
=== Création ===
Ensuite, on peut créer un squelette de "migration" :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:generate
</syntaxhighlight>
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée <code>migration_versions</code> enregistre leur succession).
==== Exemple SQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->connection->fetchAll('SHOW DATABASES;');
$this->addSql(<<<SQL
CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
}
public function down(Schema $schema) : void
{
$this->addSql('DROP TABLE ma_table');
}
}
</syntaxhighlight>
==== Exemple DQL ====
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$table = $schema->createTable('ma_table');
$table->addColumn('ma_colonne', 'string');
}
public function down(Schema $schema) : void
{
$schema->dropTable('ma_table');
}
}
</syntaxhighlight>
==== Exemple PHP ====
Depuis Symfony 7.0, il faut implémenter <code>MigrationFactory</code> pour injecter des dépendances dans les migrations (et on ne peut plus injecter tout le conteneur)<ref>https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html#migration-dependencies</ref>.
{{Boîte déroulante début|titre=Avant Symfony 7.0, il fallait juste utiliser <code>ContainerAwareTrait</code>}}
Exemple :
<syntaxhighlight lang=php>
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function up(Schema $schema) : void
{
$em = $this->container->get('doctrine.orm.entity_manager');
$monEntite = new MonEntite();
$em->persist($monEntite);
$em->flush();
}
}
</syntaxhighlight>
{{Boîte déroulante fin}}
{{attention|Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.}}
{{attention|<code>$this->container->getParameter()</code> ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple <code>$_SERVER['SUBAPI_URI']</code> renvoie la variable d'environnement et <code>$this->containergetParameter('env(SUBAPI_URI)')</code> sa valeur par défaut (définie dans services.yaml).}}
=== Exécution ===
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate
</syntaxhighlight>
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --up 20170321095644
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
</syntaxhighlight>
Pour le rollback :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:execute --down 20170321095644
</syntaxhighlight>
Pour éviter que Doctrine pose des questions durant les migrations, ajouter <code>--no-interaction</code> (ou <code>-n</code>).
Pour voir le code SQL au lieu de l'exécuter : <code>--write-sql</code>.
==== Sur plusieurs bases de données ====
Pour exécuter sur plusieurs bases :
<syntaxhighlight lang=bash>
php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
</syntaxhighlight>
Avec des migrations.yaml de type :
<syntaxhighlight lang=bash>
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
</syntaxhighlight>
=== Synchronisation ===
==== Vers le code ====
===== Vers les entités =====
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
</pre>
{{attention|Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer [[Programmation_PHP_avec_Symfony/Migration_de_Symfony_6_à_7#Rector|Rector]] pour convertir les annotations. Ex :
<pre>
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable
vendor/bin/rector process src/Entity/MyNewEntity.php
</pre>
}}
===== Vers les migrations =====
Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
==== Vers la base ====
À contrario, pour mettre à jour la BDD à partir des entités :
php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
php bin/console doctrine:schema:update --dump-sql
== Fixtures ==
Il existe plusieurs bibliothèques pour créer des {{wt|fixture}}s, dont une de Doctrine<ref>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</ref> :
<syntaxhighlight lang=bash>
composer require --dev orm-fixtures
</syntaxhighlight>
Pour charger les fixtures du code dans la base :
<syntaxhighlight lang=bash>
php bin/console doctrine:fixtures:load -n
</syntaxhighlight>
== Types de champ ==
La liste des types de champ Doctrine se trouve dans <code>Doctrine\DBAL\Types</code>. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre <code>JsonType</code> pour surcharger le type JSON par défaut afin de lui faire faire <code>json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)</code> automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne<ref>https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23</ref>.
== Réplication SQL ==
Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml.
Ex<ref>https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35</ref> :
<pre>
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
replicas:
replica1:
url: '%env(resolve:REPLICA_DATABASE_URL)%'
</pre>
== Critique ==
# Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
# Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le {{wt|lazy loading}} est contourné par au choix :
## Avant l'appel null, un <code>ObjetJoint->get()</code>.
## Dans l'entité, un <code>@ManyToOne(…, fetch="EAGER")</code>.
## Dans le repository, un <code>$this->queryBuilder->addSelect()</code>. NB : si cela ajoute un problème N+1, joindre aussi la deuxième entité qui le provoque.
# Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
# Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
# Attention aux <code>$this->queryBuilder->setMaxResults()</code> et <code>$this->queryBuilder->setFirstResult()</code> en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar du <code>LIMIT</code> SQL). La solution consiste à ajouter un paginateur<ref>https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939</ref>.
# L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
# Pas de hints.
# Bug des <code>UNION ALL</code> quand on joint deux entités non liées dans le repo.
{{todo|
* Ajouter la connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL (patrons à copier-coller ?)
}}
== Références ==
{{Références}}
85k6cj6q6251x20mkoapsvcc5pogxys
Les cartes graphiques/Les caches d'un processeur de shader
0
74269
763122
758183
2026-04-07T13:40:56Z
Mewtow
31375
/* Les caches spécialisés d'un GPU */
763122
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir comment est organisée la mémoire d'un GPU, ou plutôt devrait-on dire les mémoires d'un GPU. Eh oui : un GPU contient beaucoup de mémoires différentes. Un GPU contient évidemment une mémoire vidéo de grande taille, séparée des processeurs de shader, mais pas que. Les processeurs de shaders intègrent aussi des mémoires plus petites, appelées des mémoires caches. Les processeurs intégrent tous des caches et les processeurs de shaders ne font pas exception. Cependant, les caches d'un GPU sont quelque peu particuliers et sont organisés différemment. La hiérarchie mémoire des GPUs est assez particulière, et nous allons voir en quoi dans ce qui suit.
==Les caches spécialisés d'un GPU==
Un point important est que les GPU sont dédiés au rendu 3D, et cette spécialisation se voit dans leurs mémoires caches. Les premières cartes graphiques avaient des caches spécialisés, avec des caches pour les textures, des caches de sommets, des caches pour le tampon de profondeur, etc. Ils n'avaient pas de caches généralistes, qui servent à stocker n'importe quel type de données. Les caches spécialisés étaient intégrés aux circuits fixes. Par exemple, le cache pour les textures est placé dans l'unité de texture, le cache de sommet dans l'''input assembler'', le cache du ''z-buffer'' dans les ROPs.
===Les caches de sommets===
Avant Direct X 10, les cartes graphiques avaient des caches dédiés à la géométrie, deux précisémment. Ils étaient appelés des caches de sommets, le terme étant utilisé pour les deux caches. Le premier cache mémorise des sommets qui ont été transformés/éclairés, alors que le second mémorise des sommets pas encore éclairés. Le premier cache est appelé le ''Post-transform cache'' et se situe en sortie des unités de ''vertex shader'' ou de l'unité de T&L. Le second cache s'appelle le ''Pre-transform cache'' fait partie de l'''input assembler''. Nous détaillerons le fonctionnement de ces caches dans le chapitre sur le pipeline géométrique, nous ne pouvons pas en dire plus pour le moment.
[[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]]
===Le cache de textures===
Le '''cache de textures''', comme son nom l'indique, est un cache spécialisé dans les textures. Toutes les cartes graphiques modernes disposent de plusieurs unités de texture, qui disposent chacune de son ou ses propres caches de textures. Pas de cache partagé, ce serait peu utile et trop compliqué à implémenter.
De plus, les cartes graphiques modernes ont plusieurs caches de texture par unité de texture. Généralement, elles ont deux caches de textures : un petit cache rapide, et un gros cache lent. Les deux caches sont fortement différents. L'un est un gros cache, qui fait dans les 4 kibioctets, et l'autre est un petit cache, faisant souvent moins d'1 kibioctet. Mais le premier est plus lent que le second. Sur d'autres cartes graphiques récentes, on trouve plus de 2 caches de textures, organisés en une hiérarchie de caches de textures similaire à la hiérarchie de cache L1, L2, L3 des processeurs modernes.
Notons que ce cache interagit avec les techniques de compression de texture. Les textures sont en effet des images, qui sont donc compressées. Et elles restent compressées en mémoire vidéo, car les textures décompressées prennent beaucoup plus de place, entre 5 à 8 fois plus. Les textures sont décompressées lors des lectures : le processeur de shaders charge quelques octets, les décompresse, et utilise les données décompressées ensuite. Le cache s'introduit quelque part avant ou après la décompression.
On peut décompresser les textures avant de les placer dans le cache, ou laisser les textures compressées dans le cache. Tout est une question de compromis. Décompresser les textures dans le cache fait que la lecture dans le cache est plus rapide, car elle n'implique pas de décompression, mais le cache contient moins de données. À l'inverse, compresser les textures permet de charger plus de données dans le cache, mais rend les lectures légèrement plus lentes. C'est souvent la seconde solution qui est utilisée et ce pour deux raisons. Premièrement, la compression de texture est terriblement efficace, souvent capable de diviser par 6 la taille d'une texture, ce qui augmente drastiquement la taille effective du cache. Deuxièmement, les circuits de décompression sont généralement très rapides, très simples, et n'ajoutent que 1 à 3 cycles d'horloge lors d'une lecture.
Les anciens jeux vidéo ne faisaient que lire les textures, sans les modifier. Aussi, le cache de texture des cartes graphiques anciennes est seulement accessible en lecture, pas en écriture. Cela simplifiait fortement les circuits du cache, réduisant le nombre de transistors utilisés par le cache, réduisant sa consommation énergétique, augmentait sa rapidité, etc. Mais les jeux vidéos 3D récents utilisent des techniques dites de ''render-to-texture'', qui permettent de calculer certaines données et à les écrire en mémoire vidéo pour une utilisation ultérieure. Les textures peuvent donc être modifiées et cela se marie mal avec un cache en lecture seule.
Rendre le cache de texture accessible en écriture est une solution, mais qui demande d'ajouter beaucoup de circuits pour une utilisation somme toute peu fréquente. Une autre solution, plus adaptée, réinitialise le cache de textures quand on modifie une texture, que ce soit totalement ou partiellement. Une fois le cache vidé, les accès mémoire ultérieurs n'ont pas d'autre choix que d'aller lire la texture en mémoire et de remplir le cache avec les données chargées depuis la RAM. Les données de texture en RAM étant les bonnes, cela garantit l’absence d'erreur.
: Ces deux techniques peuvent être adaptées dans le cas où plusieurs caches de textures séparées existent sur une même carte graphique. Les écritures doivent invalider toutes les copies dans tous les caches de texture. Cela nécessite d'ajouter des circuits qui propagent l'invalidation dans tous les autres caches.
===Les caches de constante===
Un shader a besoin de certaines "constantes" pour faire son travail. Les constantes en question sont d'accès peu fréquent, qui se limite souvent à un accès au début de chaque instance de shader, guère plus. Au premier abord, dédier un cache à de telles constantes ne parait pas très utile, vu qu'elles ne semblent pas réutilisées. Mais c'est oublier un point important : toutes les instances du shader manipulent ces constantes, et il y en a souvent plusieurs qui s'exéceutn à tour de rôle, si ce n'est en même temps ! Pour profiter du partage des constantes entre instances d'un shader, les GPU incorporent des '''caches de constantes'''. Ainsi, quand un shader lit une donnée, elle est chargée dans le cache de constante, ce qui fait que les autres instances liront ces constantes depuis le cache et non depuis la VRAM.
Les caches de constante sont séparés des autres caches de données, car ce sont des données peu fréquemment utilisées, qui sont censées être évincées en priorité du cache de données, qui privilégie les données fréquemment lues/écrites. Avec un cache séparé, les constantes restent dans le cache. Au passage, ce cache de constante a des chances d'être partagé entre plusieurs cœurs, des cœurs différents ayant de fortes chances d’exécuter des instances différentes d'un même shader.
==Les caches généralistes==
Les GPUs récents contiennent des caches généralistes, qui ne sont spécialisés dans le rendu graphique. Leur existence se justifie par le fait que les GPU sont de plus en plus utilisés pour du calcul généraliste (scientifique, notamment), à savoir qu'ils exécutent des ''compute shaders'' qui manipulent des données arbitraires. Et de tels ''compute shaders'' sont parfois utilisés pour du rendu 3D, pour exécuter des algorithmes d'élimination des pixels cachés, ou des algorithmes de rendu assez complexes.
Les caches généralistes des GPU modernes ressemblent à ceux des CPU, avec une hiérarchie de caches. Pour rappel, les processeurs multicœurs modernes ont souvent trois à quatre niveaux de caches, appelés les caches L1, L2, L3 et éventuellement L4. Les GPU ont une organisation similaire, sauf que le nombre de cœurs est beaucoup plus grand que sur un processeur moderne.
* Pour le premier niveau, on a deux caches L1 par cœur/processeur : un cache pour les instructions et un cache pour les données.
* Pour le second niveau, on a un cache L2 qui peut stocker indifféremment données et instruction et qui est partagé entre plusieurs cœurs/processeurs.
* Le cache L3 est un cache partagé entre tous les cœurs/processeurs.
[[File:Partage des caches sur un processeur multicoeurs.png|centre|vignette|upright=2|Partage des caches sur un processeur multicoeurs]]
===Les caches d'instruction===
Les caches d'instruction des GPU sont adaptés aux contraintes du rendu 3D. Le principe du rendu 3D est d'appliquer un shader assez simple sur un grand nombre de données. Les shaders sont donc des programmes assez légers, qui ont peu d'instructions. Les caches d'instructions des GPU sont généralement assez petits, quelques dizaines ou centaines de kilooctets. Et malgré cela, il n'est pas rare qu'un ''shader'' tienne tout entier dans le cache d'instruction.
La seconde caractéristique est qu'un même programme s’exécute sur beaucoup de données. Il n'est pas rare que plusieurs processeurs de shaders exécutent le même ''shader''. Aussi, certains GPU partagent un même cache d’instruction entre plusieurs processeurs de ''shader'', comme c'est le cas sur les GPU AMD d'architecture GCN où un cache d'instruction de 32 kB est partagé entre 4 cœurs.
===Les caches de données===
Pour les caches de données, il faut savoir qu'un shader a peu de chances de réutiliser une donnée qu'il a chargé précédemment. Les processeurs de shaders ont beaucoup de registres, ce qui fait que si accès ultérieur à une donnée il doit y avoir, elle passe généralement par les registres. Cette faible réutilisation fait que les caches de données ne sont pas censé être très utiles. Il y a cependant des exceptions, qui expliquent que les cartes graphiques incorporent un cache de texture et un cache de sommet (pour le tampon de sommet).
Il faut noter que sur la plupart des cartes graphiques modernes, les caches de données et le cache de texture sont un seul et même cache. Même chose pour le cache de sommets, utilisé par les unités géométrique, qui est fusionné avec les caches de données. La raison est que une économie de circuits qui ne coute pas grand chose en termes de performance. Rappelons que les processeurs de shaders sont unifiés à l'heure actuelle, c'est à dire qu'elles peuvent exécuter pixel shader et vertex shader. Au lieu d'incorporer un cache de sommets et un cache de textures, autant utiliser un seul cache qui sert alternativement de cache de vertex et de cache de texture, afin d'économiser des circuits.
==La mémoire partagée : un ''local store''==
En plus d'utiliser des caches, les GPU modernes utilisent des ''local stores'', aussi appelés ''scratchpad memories''. Ce sont des mémoires RAM intermédiaires entre la RAM principale et les registres. Ces local stores peuvent être vus comme des caches, mais que le programmeur doit gérer manuellement. Dans la réalité, ce sont des mémoires RAM très rapides mais de petite taille, qui sont adressées comme n'importe quelle mémoire RAM, en utilisant des adresses directement.
[[File:Scratch-Pad-Memory.jpg|centre|vignette|upright=2.0|Scratch-Pad-Memory (SPM).]]
Sur les GPU modernes, chaque processeur de ''shader'' possède un unique ''local store'', appelée la '''mémoire partagée'''. Il n'y a pas de hiérarchie des ''local store'', similaire à la hiérarchie des caches.
[[File:Cuda5.png|centre|vignette|upright=2.0|Local stores d'un GPU.]]
La faible capacité de ces mémoires, tout du moins comparé à la grande taille de la mémoire vidéo, les rend utile pour stocker temporairement des résultats de calcul "peu imposants". L'utilité principale est donc de réduire le trafic avec la mémoire centrale, les écritures de résultats temporaires étant redirigés vers les local stores. Ils sont surtout utilisés hors du rendu 3D, pour les applications de type GPGPU, où le GPU est utilisé comme architecture multicœurs pour du calcul scientifique.
===L'implémentation des ''local store''===
Vous vous attendez certainement à ce que je dise que les ''local store'' sont des mémoires séparées des mémoires caches et qu'il y a réellement des puces de mémoire RAM distinctes dans les processeurs de ''shaders''. Mais en réalité, ce n'est pas le cas pour tous les ''local store''. Le dernier niveau de ''local store'', la mémoire partagée, est bel et bien une mémoire SRAM à part des autres, avec ses propres circuits. Mais les cartes graphiques très récentes fusionnent la mémoire locale avec le cache L1.
L'avantage est une économie de transistors assez importante. De plus, cette technologie permet de partitionner le cache/''local store'' suivant les besoins. Par exemple, si la moitié du ''local store'' est utilisé, l'autre moitié peut servir de cache L1. Si le ''local store'' n'est pas utilisé, comme c'est le cas pour la majorité des rendu 3D, le cache/''local store'' est utilisé intégralement comme cache L1.
Et si vous vous demandez comment c'est possible de fusionner un cache et une mémoire RAM, voici comment le tout est implémenté. L'implémentation consiste à couper le cache en deux circuits, dont l'un est un ''local store'', et l'autre transforme le ''local store'' en cache. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus, et ce genre de cache est parfois utilisés sur les processeurs modernes, dans des processeurs dédiés à l'embarqué ou pour certaines applications spécifiques.
Le premier circuit vérifie la présence des données à lire/écrire dans le cache. Lors d'un accès mémoire, il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Son implémentation est très variable suivant le cache considéré, mais une simple mémoire RAM suffit généralement.
En plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. La mémoire RAM de données en question n'est autre que le ''local store''. En clair, le cache s'obtient en combinant un ''local store'' avec un circuit qui s'occupe de vérifier de vérifier les succès ou défaut de cache, et qui éventuellement identifie la position de la donnée dans le cache.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Pour que le tout puisse servir alternativement de ''local store'' ou de cache, on doit contourner ou non l'unité de tags. Lors d'un accès au cache, on envoie l'adresse à lire/écrire à l'unité de tags. Lors d'un accès au ''local store'', on envoie l'adresse directement sur la mémoire RAM de données, sans intervention de l'unité de tags. Le contournement est d'autant plus simple que les adresses pour le ''local store'' sont distinctes des adresses de la mémoire vidéo, les espaces d'adressage ne sont pas les mêmes, les instructions utilisées pour lire/écrire dans ces deux mémoires sont aussi potentiellement différentes.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
Il faut préciser que cette organisation en ''phased cache'' est assez naturelle. Les caches de texture utilisent cette organisation pour diverses raisons. Vu que cache L1 et cache de texture sont le même cache, il est naturel que les caches L1 et autres aient suivi le mouvement en conservant la même organisation. La transformation du cache L1 en hydride cache/''local store'' était donc assez simple à implémenter et s'est donc faite facilement.
==La cohérence des caches sur un GPU==
Pour terminer ce chapitre, nous allons parler de la '''cohérence des caches'''. La cohérence des caches est un problème qui se manifeste à plusieurs niveaux, quand on parle d'un GPU : entre CPU et GPU, ou entre processeurs de shaders. Nous allons voir les deux cas l'un après l'autre.
===La cohérence des caches pour les transferts DMA===
Supposons que le CPU ait transféré les données dans la mémoire vidéo, avec un transfert DMA. Une situation bien précise pose problème : quand un transfert DMA écrase des données devenues inutiles, pour les remplacer par des données utiles. C'est très fréquent, les pilotes graphiques libèrent souvent de la mémoire vidéo pour la réallouer immédiatement après, afin de ne pas gaspiller de VRAM. Sans intervention du GPU, le remplacement des données aura été fait en mémoire vidéo, pas dans les caches du GPU. Et tout accès ultérieur au cache renverra la donnée écrasée.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour éviter cela, le GPU invalide ses caches en cas de transfert DMA. Par invalider, on veut dire que le cache est réinitialisé, mis à zéro, il est rendu vierge de toute donnée. Ainsi, tout accès mémoire ultérieur se fera en mémoire RAM sans passer par le cache. Les données lues depuis la RAM seront ensuite copiées dans le cache, mais ce seront les données valides écrites après le transfert DMA. Le contenu du cache est alors reconstitué au fur et à mesure des accès mémoire. L'invalidation est automatique sur les anciens GPU, elle est réalisée par le processeur de commande. Sur les GPU modernes, elle est réalisée par le programmeur, comme on va le voir dans la section immédiatement suivante.
===La cohérence des caches entre CPU et GPU===
Dans ce qui suit, on suppose qu'il n'y a qu'une seule mémoire RAM, qui est partagée entre CPU et GPU, et sert à la fois de RAM et de mémoire vidéo. Il s'agit de ce que l'on appelle la mémoire unifiée. Elle est utilisée dans de nombreuses consoles de jeu, mais aussi avec les GPU intégrés, qui sont dans le processeur. La mémoire unifiée n'implique pas de transfert DMA entre CPU et GPU, vu qu'il n'y a qu'une seule RAM. Par contre, un problème différent du précédent peut survenir.
Le processeur n'écrit pas directement en mémoire RAM, mais dans son cache. Les écritures sont propagées en RAM avec un certain retard, quand les données sont évincées du cache pour faire de la place à de nouvelles données. Et même quand elles sont propagées en RAM, les écritures ne sont pas propagées dans les caches du GPU. Pour corriger cela, lorsque le processeur envoie des données au GPU, il force le GPU à invalider ses caches. Il envoie une commande dédiée pour, qui précède les commandes liées au rendu 2D/3D/autres.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée]]
Au niveau du processeur, le processeur doit écrire les données dans la mémoire RAM, avant de faire le transfert DMA. Mais la présence de caches pose problème : les écritures peuvent être interceptées par le cache et ne pas être propagées en RAM. Pour éviter cela, les processeurs modernes marquent des blocs de mémoire comme "non-cacheables", à savoir que toute lecture/écriture dedans se fait sans passer par le cache. C'est une fonctionnalité très importante pour communiquer avec les périphériques. Pour les GPU dédiés/soudés, cela a un lien avec la mémoire vidéo mappée en mémoire. Plus haut, nous avions dit que la mémoire vidéo est visible dans l'espace d'adressage du processeur, à savoir qu'un bloc de mémoire est détourné pour adresser non pas la RAM, mais la mémoire vidéo. Et bien ce bloc de mémoire entier est marqué comme étant non-cacheable.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes]]
La situation peut être optimisée sur les GPU intégrés. Si le GPU est conçu pour, il n'y a pas besoin de marquer les données comme non-cacheables. Le cas le plus simple est celui où le CPU et le GPU partagent leur cache L3/L4. Dans ce cas, il n'y a qu'un seul cache L3/L4 qui ne contient qu'une seule copie valide, écrite par le CPU et lue par le GPU. Il faut juste garantir que la donnée soit lue par le GPU depuis le L3, mais c'est là une question d'inclusivité du cache, qui ne nous concerne pas ici. Si le CPU et le GPU ne partagent pas de cache, il suffit que le GPU puisse lire les caches du CPU. C'est la méthode utilisée sur l'APU Trinity d'AMD.
[[File:Cohérence des caches entre CPU et GPU intégré.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU intégré]]
===La cohérence des caches entre processeurs de shaders===
Pour terminer, il faut voir la cohérence des caches entre processeurs de shaders. Une carte graphique moderne est, pour simplifier, un gros processeur multicœurs auquel on aurait rajouté des ROPs, les circuits de la rastérisation et les unités de textures. Il n'est donc pas étonnant que les problèmes rencontrés sur les processeurs multicœurs soient aussi présents sur les GPU, la cohérence des caches ne fait pas exception.
Pour simplifier les explications, nous allons partir du principe que chaque processeur de shaders a son propre cache de données. Prenons deux processeur de shaders qui ont chacun une copie d'une donnée dans leur cache. Si un processeur de shaders modifie sa copie de la donnée, l'autre ne sera pas mise à jour. L'autre processeur manipule donc une donnée périmée : il n'y a pas cohérence des caches.
[[File:Cohérence des caches.png|centre|vignette|upright=2|Cohérence des caches]]
La réalité est cependant plus complexe, dans le sens où il n'y a souvent pas un cache par processeur de shaders, mais une hiérarchie de cache assez complexe, avec un cache L1 par processeur de shaders, un cache L2 partagé entre plusieurs processeur de shaders, des caches partagés entre tous les processeur de shaders, etc. Certains GPU partagent leur cache L1 d’instructions entre plusieurs processeur de shaders, d'autres non. Mais le principe reste valide, tant qu'un cache n'est pas partagé entre tous les processeurs de shaders : un cache peut contenir une donnée invalide, à savoir qu'elle a été modifiée dans le cache d'un autre processeur de shaders.
Pour corriger ce problème, les ingénieurs ont inventé des '''protocoles de cohérence des caches''' pour détecter les données périmées et les mettre à jour. Mais autant ces techniques sont faisables sur des CPU avec un nombre limité de cœurs, autant elles sont impraticables avec un GPU contenant une centaine de cœurs. Heureusement, la cohérence des caches est un problème bien moins important sur les GPU que sur les CPU. En effet, le rendu 3D implique un parallélisme de données : des processeurs/cœurs différents sont censés travailler sur des données différentes. Il est donc rare qu'une donnée soit traitée en parallèle par plusieurs cœurs, et donc qu'elle soit copiée dans plusieurs caches.
En conséquence, les GPU se contentent d'une cohérence des caches assez light, gérée par le programmeur. Si jamais une opération peut mener à un problème de cohérence des caches, le programmeur doit gérer cette situation de lui-même. Pour cela, les GPU supportent des instructions machines spécialisées, qui vident les caches. Par vider les caches, on veut dire que leur contenu est rapatrié en mémoire RAM, et qu'ils sont réinitialisés. Les accès mémoire qui suivront l'invalidation trouveront un cache vide, et devront recharger leurs données depuis la RAM. Ainsi, si une lecture/écriture peut mener à un défaut de cohérence problématique, le programmeur insère une instruction qui invalide le cache, avant l'accès mémoire potentiellement problématique. Ainsi, on garantit que la donnée chargée/écrite est lue depuis la mémoire vidéo et donc qu'il s'agit d'une donnée correcte.
Elle est utilisée pour supporter les techniques de ''render-to-texture'', pour dessiner l'image finale dans une texture (pour y appliquer un filtre de post-traitement, par exemple). Les opérations de ''render-to-texture'' étant assez rares, il vaut mieux ne pas rendre les caches de texture accessibles en écriture. L'invalidation du cache au besoin est alors parfaitement adapté. Les autres caches du GPU sont gérés avec le même principe. Pour les caches généralistes, certains GPU modernes commencent à implémenter des méthodes plus élaborées de cohérence des caches.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=La mémoire unifiée et la mémoire vidéo dédiée
| netxText=La mémoire unifiée et la mémoire vidéo dédiée
}}{{autocat}}
1acf36aorco4vihtmse4029x4vcmqzt
763123
763122
2026-04-07T13:42:37Z
Mewtow
31375
/* Les caches de sommets */
763123
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir comment est organisée la mémoire d'un GPU, ou plutôt devrait-on dire les mémoires d'un GPU. Eh oui : un GPU contient beaucoup de mémoires différentes. Un GPU contient évidemment une mémoire vidéo de grande taille, séparée des processeurs de shader, mais pas que. Les processeurs de shaders intègrent aussi des mémoires plus petites, appelées des mémoires caches. Les processeurs intégrent tous des caches et les processeurs de shaders ne font pas exception. Cependant, les caches d'un GPU sont quelque peu particuliers et sont organisés différemment. La hiérarchie mémoire des GPUs est assez particulière, et nous allons voir en quoi dans ce qui suit.
==Les caches spécialisés d'un GPU==
Un point important est que les GPU sont dédiés au rendu 3D, et cette spécialisation se voit dans leurs mémoires caches. Les premières cartes graphiques avaient des caches spécialisés, avec des caches pour les textures, des caches de sommets, des caches pour le tampon de profondeur, etc. Ils n'avaient pas de caches généralistes, qui servent à stocker n'importe quel type de données. Les caches spécialisés étaient intégrés aux circuits fixes. Par exemple, le cache pour les textures est placé dans l'unité de texture, le cache de sommet dans l'''input assembler'', le cache du ''z-buffer'' dans les ROPs.
===Les caches de sommets===
Avant Direct X 10, les cartes graphiques avaient des caches dédiés à la géométrie, deux précisémment. Ils étaient appelés des caches de sommets, le terme étant utilisé pour les deux caches. Le premier cache mémorise des sommets qui ont été transformés/éclairés, alors que le second mémorise des sommets pas encore éclairés. Le premier cache est appelé le ''Post-transform cache'' et se situe en sortie des unités de ''vertex shader'' ou de l'unité de T&L. Le second cache s'appelle le ''Pre-transform cache'' fait partie de l'''input assembler''.
[[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]]
Nous détaillerons le fonctionnement de ces caches dans le chapitre sur le pipeline géométrique, nous ne pouvons pas en dire plus pour le moment. De plus, les deux caches ont disparus sur certains GPU modernes. Le ''Pre Transform Cache'' a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. Le ''Post-transform cache'' a lieu été remplacé, en raison de la manière dont les processeurs de shaders fonctionnent. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet.
===Le cache de textures===
Le '''cache de textures''', comme son nom l'indique, est un cache spécialisé dans les textures. Toutes les cartes graphiques modernes disposent de plusieurs unités de texture, qui disposent chacune de son ou ses propres caches de textures. Pas de cache partagé, ce serait peu utile et trop compliqué à implémenter.
De plus, les cartes graphiques modernes ont plusieurs caches de texture par unité de texture. Généralement, elles ont deux caches de textures : un petit cache rapide, et un gros cache lent. Les deux caches sont fortement différents. L'un est un gros cache, qui fait dans les 4 kibioctets, et l'autre est un petit cache, faisant souvent moins d'1 kibioctet. Mais le premier est plus lent que le second. Sur d'autres cartes graphiques récentes, on trouve plus de 2 caches de textures, organisés en une hiérarchie de caches de textures similaire à la hiérarchie de cache L1, L2, L3 des processeurs modernes.
Notons que ce cache interagit avec les techniques de compression de texture. Les textures sont en effet des images, qui sont donc compressées. Et elles restent compressées en mémoire vidéo, car les textures décompressées prennent beaucoup plus de place, entre 5 à 8 fois plus. Les textures sont décompressées lors des lectures : le processeur de shaders charge quelques octets, les décompresse, et utilise les données décompressées ensuite. Le cache s'introduit quelque part avant ou après la décompression.
On peut décompresser les textures avant de les placer dans le cache, ou laisser les textures compressées dans le cache. Tout est une question de compromis. Décompresser les textures dans le cache fait que la lecture dans le cache est plus rapide, car elle n'implique pas de décompression, mais le cache contient moins de données. À l'inverse, compresser les textures permet de charger plus de données dans le cache, mais rend les lectures légèrement plus lentes. C'est souvent la seconde solution qui est utilisée et ce pour deux raisons. Premièrement, la compression de texture est terriblement efficace, souvent capable de diviser par 6 la taille d'une texture, ce qui augmente drastiquement la taille effective du cache. Deuxièmement, les circuits de décompression sont généralement très rapides, très simples, et n'ajoutent que 1 à 3 cycles d'horloge lors d'une lecture.
Les anciens jeux vidéo ne faisaient que lire les textures, sans les modifier. Aussi, le cache de texture des cartes graphiques anciennes est seulement accessible en lecture, pas en écriture. Cela simplifiait fortement les circuits du cache, réduisant le nombre de transistors utilisés par le cache, réduisant sa consommation énergétique, augmentait sa rapidité, etc. Mais les jeux vidéos 3D récents utilisent des techniques dites de ''render-to-texture'', qui permettent de calculer certaines données et à les écrire en mémoire vidéo pour une utilisation ultérieure. Les textures peuvent donc être modifiées et cela se marie mal avec un cache en lecture seule.
Rendre le cache de texture accessible en écriture est une solution, mais qui demande d'ajouter beaucoup de circuits pour une utilisation somme toute peu fréquente. Une autre solution, plus adaptée, réinitialise le cache de textures quand on modifie une texture, que ce soit totalement ou partiellement. Une fois le cache vidé, les accès mémoire ultérieurs n'ont pas d'autre choix que d'aller lire la texture en mémoire et de remplir le cache avec les données chargées depuis la RAM. Les données de texture en RAM étant les bonnes, cela garantit l’absence d'erreur.
: Ces deux techniques peuvent être adaptées dans le cas où plusieurs caches de textures séparées existent sur une même carte graphique. Les écritures doivent invalider toutes les copies dans tous les caches de texture. Cela nécessite d'ajouter des circuits qui propagent l'invalidation dans tous les autres caches.
===Les caches de constante===
Un shader a besoin de certaines "constantes" pour faire son travail. Les constantes en question sont d'accès peu fréquent, qui se limite souvent à un accès au début de chaque instance de shader, guère plus. Au premier abord, dédier un cache à de telles constantes ne parait pas très utile, vu qu'elles ne semblent pas réutilisées. Mais c'est oublier un point important : toutes les instances du shader manipulent ces constantes, et il y en a souvent plusieurs qui s'exéceutn à tour de rôle, si ce n'est en même temps ! Pour profiter du partage des constantes entre instances d'un shader, les GPU incorporent des '''caches de constantes'''. Ainsi, quand un shader lit une donnée, elle est chargée dans le cache de constante, ce qui fait que les autres instances liront ces constantes depuis le cache et non depuis la VRAM.
Les caches de constante sont séparés des autres caches de données, car ce sont des données peu fréquemment utilisées, qui sont censées être évincées en priorité du cache de données, qui privilégie les données fréquemment lues/écrites. Avec un cache séparé, les constantes restent dans le cache. Au passage, ce cache de constante a des chances d'être partagé entre plusieurs cœurs, des cœurs différents ayant de fortes chances d’exécuter des instances différentes d'un même shader.
==Les caches généralistes==
Les GPUs récents contiennent des caches généralistes, qui ne sont spécialisés dans le rendu graphique. Leur existence se justifie par le fait que les GPU sont de plus en plus utilisés pour du calcul généraliste (scientifique, notamment), à savoir qu'ils exécutent des ''compute shaders'' qui manipulent des données arbitraires. Et de tels ''compute shaders'' sont parfois utilisés pour du rendu 3D, pour exécuter des algorithmes d'élimination des pixels cachés, ou des algorithmes de rendu assez complexes.
Les caches généralistes des GPU modernes ressemblent à ceux des CPU, avec une hiérarchie de caches. Pour rappel, les processeurs multicœurs modernes ont souvent trois à quatre niveaux de caches, appelés les caches L1, L2, L3 et éventuellement L4. Les GPU ont une organisation similaire, sauf que le nombre de cœurs est beaucoup plus grand que sur un processeur moderne.
* Pour le premier niveau, on a deux caches L1 par cœur/processeur : un cache pour les instructions et un cache pour les données.
* Pour le second niveau, on a un cache L2 qui peut stocker indifféremment données et instruction et qui est partagé entre plusieurs cœurs/processeurs.
* Le cache L3 est un cache partagé entre tous les cœurs/processeurs.
[[File:Partage des caches sur un processeur multicoeurs.png|centre|vignette|upright=2|Partage des caches sur un processeur multicoeurs]]
===Les caches d'instruction===
Les caches d'instruction des GPU sont adaptés aux contraintes du rendu 3D. Le principe du rendu 3D est d'appliquer un shader assez simple sur un grand nombre de données. Les shaders sont donc des programmes assez légers, qui ont peu d'instructions. Les caches d'instructions des GPU sont généralement assez petits, quelques dizaines ou centaines de kilooctets. Et malgré cela, il n'est pas rare qu'un ''shader'' tienne tout entier dans le cache d'instruction.
La seconde caractéristique est qu'un même programme s’exécute sur beaucoup de données. Il n'est pas rare que plusieurs processeurs de shaders exécutent le même ''shader''. Aussi, certains GPU partagent un même cache d’instruction entre plusieurs processeurs de ''shader'', comme c'est le cas sur les GPU AMD d'architecture GCN où un cache d'instruction de 32 kB est partagé entre 4 cœurs.
===Les caches de données===
Pour les caches de données, il faut savoir qu'un shader a peu de chances de réutiliser une donnée qu'il a chargé précédemment. Les processeurs de shaders ont beaucoup de registres, ce qui fait que si accès ultérieur à une donnée il doit y avoir, elle passe généralement par les registres. Cette faible réutilisation fait que les caches de données ne sont pas censé être très utiles. Il y a cependant des exceptions, qui expliquent que les cartes graphiques incorporent un cache de texture et un cache de sommet (pour le tampon de sommet).
Il faut noter que sur la plupart des cartes graphiques modernes, les caches de données et le cache de texture sont un seul et même cache. Même chose pour le cache de sommets, utilisé par les unités géométrique, qui est fusionné avec les caches de données. La raison est que une économie de circuits qui ne coute pas grand chose en termes de performance. Rappelons que les processeurs de shaders sont unifiés à l'heure actuelle, c'est à dire qu'elles peuvent exécuter pixel shader et vertex shader. Au lieu d'incorporer un cache de sommets et un cache de textures, autant utiliser un seul cache qui sert alternativement de cache de vertex et de cache de texture, afin d'économiser des circuits.
==La mémoire partagée : un ''local store''==
En plus d'utiliser des caches, les GPU modernes utilisent des ''local stores'', aussi appelés ''scratchpad memories''. Ce sont des mémoires RAM intermédiaires entre la RAM principale et les registres. Ces local stores peuvent être vus comme des caches, mais que le programmeur doit gérer manuellement. Dans la réalité, ce sont des mémoires RAM très rapides mais de petite taille, qui sont adressées comme n'importe quelle mémoire RAM, en utilisant des adresses directement.
[[File:Scratch-Pad-Memory.jpg|centre|vignette|upright=2.0|Scratch-Pad-Memory (SPM).]]
Sur les GPU modernes, chaque processeur de ''shader'' possède un unique ''local store'', appelée la '''mémoire partagée'''. Il n'y a pas de hiérarchie des ''local store'', similaire à la hiérarchie des caches.
[[File:Cuda5.png|centre|vignette|upright=2.0|Local stores d'un GPU.]]
La faible capacité de ces mémoires, tout du moins comparé à la grande taille de la mémoire vidéo, les rend utile pour stocker temporairement des résultats de calcul "peu imposants". L'utilité principale est donc de réduire le trafic avec la mémoire centrale, les écritures de résultats temporaires étant redirigés vers les local stores. Ils sont surtout utilisés hors du rendu 3D, pour les applications de type GPGPU, où le GPU est utilisé comme architecture multicœurs pour du calcul scientifique.
===L'implémentation des ''local store''===
Vous vous attendez certainement à ce que je dise que les ''local store'' sont des mémoires séparées des mémoires caches et qu'il y a réellement des puces de mémoire RAM distinctes dans les processeurs de ''shaders''. Mais en réalité, ce n'est pas le cas pour tous les ''local store''. Le dernier niveau de ''local store'', la mémoire partagée, est bel et bien une mémoire SRAM à part des autres, avec ses propres circuits. Mais les cartes graphiques très récentes fusionnent la mémoire locale avec le cache L1.
L'avantage est une économie de transistors assez importante. De plus, cette technologie permet de partitionner le cache/''local store'' suivant les besoins. Par exemple, si la moitié du ''local store'' est utilisé, l'autre moitié peut servir de cache L1. Si le ''local store'' n'est pas utilisé, comme c'est le cas pour la majorité des rendu 3D, le cache/''local store'' est utilisé intégralement comme cache L1.
Et si vous vous demandez comment c'est possible de fusionner un cache et une mémoire RAM, voici comment le tout est implémenté. L'implémentation consiste à couper le cache en deux circuits, dont l'un est un ''local store'', et l'autre transforme le ''local store'' en cache. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus, et ce genre de cache est parfois utilisés sur les processeurs modernes, dans des processeurs dédiés à l'embarqué ou pour certaines applications spécifiques.
Le premier circuit vérifie la présence des données à lire/écrire dans le cache. Lors d'un accès mémoire, il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Son implémentation est très variable suivant le cache considéré, mais une simple mémoire RAM suffit généralement.
En plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. La mémoire RAM de données en question n'est autre que le ''local store''. En clair, le cache s'obtient en combinant un ''local store'' avec un circuit qui s'occupe de vérifier de vérifier les succès ou défaut de cache, et qui éventuellement identifie la position de la donnée dans le cache.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Pour que le tout puisse servir alternativement de ''local store'' ou de cache, on doit contourner ou non l'unité de tags. Lors d'un accès au cache, on envoie l'adresse à lire/écrire à l'unité de tags. Lors d'un accès au ''local store'', on envoie l'adresse directement sur la mémoire RAM de données, sans intervention de l'unité de tags. Le contournement est d'autant plus simple que les adresses pour le ''local store'' sont distinctes des adresses de la mémoire vidéo, les espaces d'adressage ne sont pas les mêmes, les instructions utilisées pour lire/écrire dans ces deux mémoires sont aussi potentiellement différentes.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
Il faut préciser que cette organisation en ''phased cache'' est assez naturelle. Les caches de texture utilisent cette organisation pour diverses raisons. Vu que cache L1 et cache de texture sont le même cache, il est naturel que les caches L1 et autres aient suivi le mouvement en conservant la même organisation. La transformation du cache L1 en hydride cache/''local store'' était donc assez simple à implémenter et s'est donc faite facilement.
==La cohérence des caches sur un GPU==
Pour terminer ce chapitre, nous allons parler de la '''cohérence des caches'''. La cohérence des caches est un problème qui se manifeste à plusieurs niveaux, quand on parle d'un GPU : entre CPU et GPU, ou entre processeurs de shaders. Nous allons voir les deux cas l'un après l'autre.
===La cohérence des caches pour les transferts DMA===
Supposons que le CPU ait transféré les données dans la mémoire vidéo, avec un transfert DMA. Une situation bien précise pose problème : quand un transfert DMA écrase des données devenues inutiles, pour les remplacer par des données utiles. C'est très fréquent, les pilotes graphiques libèrent souvent de la mémoire vidéo pour la réallouer immédiatement après, afin de ne pas gaspiller de VRAM. Sans intervention du GPU, le remplacement des données aura été fait en mémoire vidéo, pas dans les caches du GPU. Et tout accès ultérieur au cache renverra la donnée écrasée.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour éviter cela, le GPU invalide ses caches en cas de transfert DMA. Par invalider, on veut dire que le cache est réinitialisé, mis à zéro, il est rendu vierge de toute donnée. Ainsi, tout accès mémoire ultérieur se fera en mémoire RAM sans passer par le cache. Les données lues depuis la RAM seront ensuite copiées dans le cache, mais ce seront les données valides écrites après le transfert DMA. Le contenu du cache est alors reconstitué au fur et à mesure des accès mémoire. L'invalidation est automatique sur les anciens GPU, elle est réalisée par le processeur de commande. Sur les GPU modernes, elle est réalisée par le programmeur, comme on va le voir dans la section immédiatement suivante.
===La cohérence des caches entre CPU et GPU===
Dans ce qui suit, on suppose qu'il n'y a qu'une seule mémoire RAM, qui est partagée entre CPU et GPU, et sert à la fois de RAM et de mémoire vidéo. Il s'agit de ce que l'on appelle la mémoire unifiée. Elle est utilisée dans de nombreuses consoles de jeu, mais aussi avec les GPU intégrés, qui sont dans le processeur. La mémoire unifiée n'implique pas de transfert DMA entre CPU et GPU, vu qu'il n'y a qu'une seule RAM. Par contre, un problème différent du précédent peut survenir.
Le processeur n'écrit pas directement en mémoire RAM, mais dans son cache. Les écritures sont propagées en RAM avec un certain retard, quand les données sont évincées du cache pour faire de la place à de nouvelles données. Et même quand elles sont propagées en RAM, les écritures ne sont pas propagées dans les caches du GPU. Pour corriger cela, lorsque le processeur envoie des données au GPU, il force le GPU à invalider ses caches. Il envoie une commande dédiée pour, qui précède les commandes liées au rendu 2D/3D/autres.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée]]
Au niveau du processeur, le processeur doit écrire les données dans la mémoire RAM, avant de faire le transfert DMA. Mais la présence de caches pose problème : les écritures peuvent être interceptées par le cache et ne pas être propagées en RAM. Pour éviter cela, les processeurs modernes marquent des blocs de mémoire comme "non-cacheables", à savoir que toute lecture/écriture dedans se fait sans passer par le cache. C'est une fonctionnalité très importante pour communiquer avec les périphériques. Pour les GPU dédiés/soudés, cela a un lien avec la mémoire vidéo mappée en mémoire. Plus haut, nous avions dit que la mémoire vidéo est visible dans l'espace d'adressage du processeur, à savoir qu'un bloc de mémoire est détourné pour adresser non pas la RAM, mais la mémoire vidéo. Et bien ce bloc de mémoire entier est marqué comme étant non-cacheable.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes]]
La situation peut être optimisée sur les GPU intégrés. Si le GPU est conçu pour, il n'y a pas besoin de marquer les données comme non-cacheables. Le cas le plus simple est celui où le CPU et le GPU partagent leur cache L3/L4. Dans ce cas, il n'y a qu'un seul cache L3/L4 qui ne contient qu'une seule copie valide, écrite par le CPU et lue par le GPU. Il faut juste garantir que la donnée soit lue par le GPU depuis le L3, mais c'est là une question d'inclusivité du cache, qui ne nous concerne pas ici. Si le CPU et le GPU ne partagent pas de cache, il suffit que le GPU puisse lire les caches du CPU. C'est la méthode utilisée sur l'APU Trinity d'AMD.
[[File:Cohérence des caches entre CPU et GPU intégré.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU intégré]]
===La cohérence des caches entre processeurs de shaders===
Pour terminer, il faut voir la cohérence des caches entre processeurs de shaders. Une carte graphique moderne est, pour simplifier, un gros processeur multicœurs auquel on aurait rajouté des ROPs, les circuits de la rastérisation et les unités de textures. Il n'est donc pas étonnant que les problèmes rencontrés sur les processeurs multicœurs soient aussi présents sur les GPU, la cohérence des caches ne fait pas exception.
Pour simplifier les explications, nous allons partir du principe que chaque processeur de shaders a son propre cache de données. Prenons deux processeur de shaders qui ont chacun une copie d'une donnée dans leur cache. Si un processeur de shaders modifie sa copie de la donnée, l'autre ne sera pas mise à jour. L'autre processeur manipule donc une donnée périmée : il n'y a pas cohérence des caches.
[[File:Cohérence des caches.png|centre|vignette|upright=2|Cohérence des caches]]
La réalité est cependant plus complexe, dans le sens où il n'y a souvent pas un cache par processeur de shaders, mais une hiérarchie de cache assez complexe, avec un cache L1 par processeur de shaders, un cache L2 partagé entre plusieurs processeur de shaders, des caches partagés entre tous les processeur de shaders, etc. Certains GPU partagent leur cache L1 d’instructions entre plusieurs processeur de shaders, d'autres non. Mais le principe reste valide, tant qu'un cache n'est pas partagé entre tous les processeurs de shaders : un cache peut contenir une donnée invalide, à savoir qu'elle a été modifiée dans le cache d'un autre processeur de shaders.
Pour corriger ce problème, les ingénieurs ont inventé des '''protocoles de cohérence des caches''' pour détecter les données périmées et les mettre à jour. Mais autant ces techniques sont faisables sur des CPU avec un nombre limité de cœurs, autant elles sont impraticables avec un GPU contenant une centaine de cœurs. Heureusement, la cohérence des caches est un problème bien moins important sur les GPU que sur les CPU. En effet, le rendu 3D implique un parallélisme de données : des processeurs/cœurs différents sont censés travailler sur des données différentes. Il est donc rare qu'une donnée soit traitée en parallèle par plusieurs cœurs, et donc qu'elle soit copiée dans plusieurs caches.
En conséquence, les GPU se contentent d'une cohérence des caches assez light, gérée par le programmeur. Si jamais une opération peut mener à un problème de cohérence des caches, le programmeur doit gérer cette situation de lui-même. Pour cela, les GPU supportent des instructions machines spécialisées, qui vident les caches. Par vider les caches, on veut dire que leur contenu est rapatrié en mémoire RAM, et qu'ils sont réinitialisés. Les accès mémoire qui suivront l'invalidation trouveront un cache vide, et devront recharger leurs données depuis la RAM. Ainsi, si une lecture/écriture peut mener à un défaut de cohérence problématique, le programmeur insère une instruction qui invalide le cache, avant l'accès mémoire potentiellement problématique. Ainsi, on garantit que la donnée chargée/écrite est lue depuis la mémoire vidéo et donc qu'il s'agit d'une donnée correcte.
Elle est utilisée pour supporter les techniques de ''render-to-texture'', pour dessiner l'image finale dans une texture (pour y appliquer un filtre de post-traitement, par exemple). Les opérations de ''render-to-texture'' étant assez rares, il vaut mieux ne pas rendre les caches de texture accessibles en écriture. L'invalidation du cache au besoin est alors parfaitement adapté. Les autres caches du GPU sont gérés avec le même principe. Pour les caches généralistes, certains GPU modernes commencent à implémenter des méthodes plus élaborées de cohérence des caches.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=La mémoire unifiée et la mémoire vidéo dédiée
| netxText=La mémoire unifiée et la mémoire vidéo dédiée
}}{{autocat}}
5ilsjgc68heukfbs1i9ow6q4bh95bye
Mathc initiation/Fichiers h : c50a1
0
76611
763221
762488
2026-04-07T19:46:19Z
Xhungab
23827
763221
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005h| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de surface}}
:
En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c46a5|x_hfile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/Fichiers h : c46a6|x_sdxy.h .............. L'intégrale de surface]]
* [[Mathc initiation/Fichiers h : c46a7|x_sdyx.h .............. L'intégrale de surface]]
:
<br>
les fonctions f :
* [[Mathc initiation/Fichiers h : c65a5|f.h]]
:
<br>
Exemples d'application : La forme réduite ne permet que de calculer l'aire de la surface.
* [[Mathc initiation/Fichiers h : c59ad|c00a.c .... dxdy ]]
* [[Mathc initiation/c34a4|c00b.c .... dxdy ]]
* [[Mathc initiation/Fichiers h : c50a5|c00c.c .... dydx ]]
* [[Mathc initiation/Fichiers h : c50a6|c00d.c .... dydx ]]
:
{{AutoCat}}
74ejcdp6i5gmuqr17c08roqhy22nhm4
763222
763221
2026-04-07T19:46:52Z
Xhungab
23827
763222
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005h| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de surface (forme explicite réduite)}}
:
En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c46a5|x_hfile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/Fichiers h : c46a6|x_sdxy.h .............. L'intégrale de surface]]
* [[Mathc initiation/Fichiers h : c46a7|x_sdyx.h .............. L'intégrale de surface]]
:
<br>
les fonctions f :
* [[Mathc initiation/Fichiers h : c65a5|f.h]]
:
<br>
Exemples d'application : La forme réduite ne permet que de calculer l'aire de la surface.
* [[Mathc initiation/Fichiers h : c59ad|c00a.c .... dxdy ]]
* [[Mathc initiation/c34a4|c00b.c .... dxdy ]]
* [[Mathc initiation/Fichiers h : c50a5|c00c.c .... dydx ]]
* [[Mathc initiation/Fichiers h : c50a6|c00d.c .... dydx ]]
:
{{AutoCat}}
l67ojpg81xvf44u5dmfd96vrquz5qic
Mathc initiation/Fichiers h : c60
0
76733
763223
762490
2026-04-07T19:57:09Z
Xhungab
23827
763223
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/005h| Sommaire]]
:
{{Partie{{{type|}}}| L'intégrale de surface (forme explicite)}}
:
En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [[https://en.wikipedia.org/wiki/Surface_integral wikipedia]]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c60a1|x_hfile.h ............ Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]]
* [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]]
* [[Mathc initiation/Fichiers h : c60a5|x_srfxy.h ............ Calculer l'intégrale de surface en xy]]
* [[Mathc initiation/Fichiers h : c60a6|x_srfxz.h ............ Calculer l'intégrale de surface en xz]]
* [[Mathc initiation/Fichiers h : c60a7|x_srfyx.h ............ Calculer l'intégrale de surface en yx]]
* [[Mathc initiation/Fichiers h : c60a8|x_srfyz.h ............ Calculer l'intégrale de surface en yz]]
* [[Mathc initiation/Fichiers h : c60a9|x_srfzx.h ............ Calculer l'intégrale de surface en zx]]
* [[Mathc initiation/Fichiers h : c60aa|x_srfzy.h ............ Calculer l'intégrale de surface en zy]]
:
<br>
les fonctions f :
* [[Mathc initiation/Fichiers h : c60fa|f.h]]
:
<br>
Exemples d'application :
* La forme explicite permet de calculer l'aire de la surface si f(x,y,s) =1. Elle permet de calculer la masse de la surface si f(x,y,s) est positive.
* [[Mathc initiation/Fichiers c : c60ca|c18a.c ............ ex : en xy ]]
* [[Mathc initiation/Fichiers c : c60cb|c18b.c ............ ex : en xz ]]
* [[Mathc initiation/Fichiers c : c60cc|c18c.c ............ ex : en yx ]]
* [[Mathc initiation/Fichiers c : c60cd|c18d.c ............ ex : en yz ]]
* [[Mathc initiation/Fichiers c : c60ce|c18e.c ............ ex : en zx ]]
* [[Mathc initiation/Fichiers c : c60cf|c18f.c ............ ex : en zy ]]
:
<br>
Regardons la fonction qui effectue le travail :
* [[Mathc initiation/a311| Étudions la fonction '''surface_dxdy();''']]
:
{{AutoCat}}
qx0oxhm8effn9vcjsix56yidj73xlbk
Les cartes graphiques/Le rendu d'une scène 3D : concepts de base
0
79234
763172
763055
2026-04-07T16:02:06Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763172
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 totale} = 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}}
tu7g5p51v5218s4lvrmby1d47r8xl7u
763174
763172
2026-04-07T16:05:24Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763174
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 totale} = 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}}
kfbf2990f12lar4jslss453tmscnlsl
763176
763174
2026-04-07T16:18:30Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763176
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 plusieurs 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 la 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 lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La 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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
[[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 totale} = 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}}
nt9mllzsuwft9cz9femslewchgl2tbg
763177
763176
2026-04-07T16:23:00Z
Mewtow
31375
/* L'éclairage d'une scène 3D */
763177
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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 plusieurs 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 la 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 lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La 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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
[[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 totale} = 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}}
7exx6t3oackv84coeyft613uedx9m79
763178
763177
2026-04-07T16:25:32Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763178
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
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.]]
|}
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.
Le calcul exact de l'illumination de chaque point de surface demande de calculer plusieurs 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 la 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 lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La 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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
[[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 totale} = 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}}
d1gv8vc2ion9417zqpkmbtg2qsfswfe
763179
763178
2026-04-07T16:30:43Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763179
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
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.]]
|}
Les sources de lumière ont une couleur bien précise (la couleur de la lumière émise) et émettent une intensité lumineuse codée par un entier.
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.
Le calcul exact de l'illumination de chaque point de surface demande de calculer plusieurs 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 la 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 lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La 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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
[[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 totale} = 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}}
ayi1ajt7ngz0g0ecqq739tiona72o6r
763180
763179
2026-04-07T16:34:19Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763180
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''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.]]
|}
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
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.
Le calcul exact de l'illumination de chaque point de surface demande de calculer plusieurs 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 la 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 lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La 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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
[[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 totale} = 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}}
6dhbxu7ph10xdon3ms488ggfnonohn2
763181
763180
2026-04-07T16:39:16Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763181
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''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.]]
|}
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité, l'intensité de la lumière réfléchie par la surface. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière provenant des sources de lumières. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 totale} = 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}}
bmzr26mqdbfuvdnamjiwtvitf94m910
763182
763181
2026-04-07T16:56:37Z
Mewtow
31375
/* L'éclairage d'une scène 3D */
763182
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''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.]]
|}
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité, l'intensité de la lumière réfléchie par la surface. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière provenant des sources de lumières. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Si on suppose que la lumière est ponctuelle, on a :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas ddu sommet considéré.
[[File:Graphics lightmodel ptsource.png|centre|vignette|Normale de la surface.]]
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 totale} = 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}}
swu4fgl7ms4z168a2s3xqznj77jdakd
763183
763182
2026-04-07T16:57:15Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763183
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité, l'intensité de la lumière réfléchie par la surface. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière provenant des sources de lumières. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Si on suppose que la lumière est ponctuelle, on a :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas ddu sommet considéré.
[[File:Graphics lightmodel ptsource.png|centre|vignette|Normale de la surface.]]
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 totale} = 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}}
g75vw1u0lc4vtgzrazbmnjw4u2uclhl
763184
763183
2026-04-07T16:58:11Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763184
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière provenant des sources de lumières. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Si on suppose que la lumière est ponctuelle, on a :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas ddu sommet considéré.
[[File:Graphics lightmodel ptsource.png|centre|vignette|Normale de la surface.]]
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 totale} = 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}}
nyxh4mb5qo9cg5s3urmqsb6e5y3pvj0
763185
763184
2026-04-07T17:02:20Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763185
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière provenant des sources de lumières. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. 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.]]
===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 totale} = 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}}
q8atttztln5nqdl9cffkfz58l0lgf9c
763187
763185
2026-04-07T17:08:18Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763187
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principales étant les deux suivantes :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
Dans la réalité, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. 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.]]
===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 totale} = 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}}
t2e4w6ccxh30krhpnh30b5g1ojr4vsd
763188
763187
2026-04-07T17:08:48Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763188
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
Dans la réalité, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. 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.]]
===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 totale} = 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}}
4913zfxi8ifahaj2p5zvd31drlw0vwl
763189
763188
2026-04-07T17:10:23Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763189
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 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 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 la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. 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.]]
===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 totale} = 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}}
mglz8d2r4rf452wx30qmq5q1shxc6vj
763190
763189
2026-04-07T17:12:33Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763190
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. 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.]]
===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 totale} = 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}}
hhcddsqk2ssus3ccg75av145lao7v9x
763191
763190
2026-04-07T17:13:34Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763191
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie apr la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur, noté R et non-indiqué dans les schéma au-dessus, se calcule à partir du vecteur L et de la normale.
===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 totale} = 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}}
4gwt6q6z7ed118i65yq9jvpx1b5sep6
763193
763191
2026-04-07T17:24:46Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763193
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie apr la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur, noté R et non-indiqué dans les schéma au-dessus, se calcule à partir du vecteur L et de la normale.
===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 en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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 totale} = 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}}
qda4x12h0b4ncc35itu6yu7oqbyze5l
763194
763193
2026-04-07T17:27:37Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763194
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie apr la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur, noté R et non-indiqué dans les schéma au-dessus, se calcule à partir du vecteur L et de la normale.
À 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 en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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 totale} = 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}}
38ip4c6o33h46tolaol9vm8knf5s83g
763195
763194
2026-04-07T17:29:38Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763195
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. 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>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = 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}}
7byvlcxhrvirkv0p5ztofupswzbyqrd
763196
763195
2026-04-07T17:33:23Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763196
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents.
L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). Une bonne approximation dit qu'elle est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet.
Reste à calculer ce cosinus, et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent 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>
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. 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>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = 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}}
5nd0zbka1fnskoxz0v7cznt99thad4m
763197
763196
2026-04-07T17:34:05Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763197
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. Une bonne approximation dit que l'illumination diffuse est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet.
Reste à calculer ce cosinus, et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent 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>
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. 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>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = 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}}
caovhv8h3of4cl8gd7ks73nha0j2dd6
763198
763197
2026-04-07T17:37:11Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763198
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>. Une bonne approximation dit que l'illumination diffuse est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet :
: <math>\text{Illumination diffuse} = K_d \times I_a \times \cos{(\omega)}</math>
Reste à calculer ce cosinus, et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent 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 de deux vecteurs A et B} = \vec{A} \cdot \vec{B}= A \times B \times \cos{(\omega)}</math>
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = 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>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = 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}}
84pi08beqex76u22sspfch9nh8g3uyj
763199
763198
2026-04-07T17:45:00Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763199
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>. Une bonne approximation dit que l'illumination diffuse est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet :
: <math>\text{Illumination diffuse} = K_d \times I_a \times \cos{(\omega)}</math>
Reste à calculer ce cosinus. Et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire agit sur 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 de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I_a \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusiuers manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I_a \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \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}}
ckcpxafribp0bexkjc7opn03q5z2eb3
763200
763199
2026-04-07T17:45:19Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763200
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>. Une bonne approximation dit que l'illumination diffuse est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet :
: <math>\text{Illumination diffuse} = K_d \times I \times \cos{(\omega)}</math>
Reste à calculer ce cosinus. Et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire agit sur 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 de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \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}}
k8w3v5c5xt55nsrli5ql6wf7ruiiqz2
763201
763200
2026-04-07T17:46:13Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763201
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>. Une bonne approximation dit que l'illumination diffuse est proportionnelle au cosinus de cet angle. Il suffit alors de multiplier ce cosinus par l'intensité de la lumière et la couleur diffuse du sommet :
: <math>\text{Illumination diffuse} = K_d \times I \times \cos{(\omega)}</math>
Reste à calculer ce cosinus. Et si possible sans utiliser de calculs trigonométriques très gourmands pour le matériel. Pour cela, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire agit sur 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 de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \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}}
lsqdc69fzlrwnv2n6s9z5r7vsxe4aqf
763202
763201
2026-04-07T18:04:22Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763202
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===Le produit scalaire===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur 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 est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,yz est le suivant :
: <math>\text{Produit scalaire de deux vecteurs A et B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \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}}
e0rglx35mvahddsx77uby2cydqnk8ym
763203
763202
2026-04-07T18:04:47Z
Mewtow
31375
/* Le produit scalaire */
763203
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur 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 est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,yz est le suivant :
: <math>\text{Produit scalaire de deux vecteurs A et B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \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}}
t7nq20wfw7rthbcn3y97tuid1fb4aby
763204
763203
2026-04-07T18:05:18Z
Mewtow
31375
/* Le produit scalaire de deux vecteurs */
763204
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur 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 est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,yz est le suivant :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \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}}
q2kcmjky6yid7qi3ip8gx6qmmub5jwa
763205
763204
2026-04-07T18:05:53Z
Mewtow
31375
/* Le produit scalaire de deux vecteurs */
763205
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é de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet 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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise.
* Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
En théorie, la lumière rebondit sur les surface et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet 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.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* 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. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[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.]]
L'illumination diffuse et spéculaire sont calculées séparément et 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.]]
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). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, 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 calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils 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 impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, il est modifié lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[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).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, 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.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur 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 est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B.
L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué.
Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire.
===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 spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). 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. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination 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.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \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}}
dhie3bg3w9pdqvxhkvru4lzlr37d3li
Les cartes graphiques/La microarchitecture des processeurs de shaders
0
81538
763208
758127
2026-04-07T18:30:34Z
Mewtow
31375
763208
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===L'unité de texture/lecture/écriture===
L'unité d'accès mémoire s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
==La gestion des dépendances de données==
Un processeur de ''shaders'' SIMD contient donc beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
bvbrqw18k9uvm7zklepxq4tqoaax2jd
763209
763208
2026-04-07T18:35:22Z
Mewtow
31375
/* L'unité de texture/lecture/écriture */
763209
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Pour résumer, un processeur de shader contient de nombreuses unités de calcul spécialisées. Le schéma précédent résume le tout. Il montre l'unité de controle (en haut), une unité mémoire (LSU), une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des registres, elle montre qu'il y a trois bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD, et une mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==La gestion des dépendances de données==
Un processeur de ''shaders'' SIMD contient donc beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
mzkkfmv3uccw57ys8hak8jh4wy2kbes
763210
763209
2026-04-07T18:41:33Z
Mewtow
31375
/* Les autres circuits, et résumé */
763210
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut, elle n'est pas détaillée, mais elle est composée de plusieurs circuits qui s'enchaine t l'un à la suite de l'autre. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==La gestion des dépendances de données==
Un processeur de ''shaders'' SIMD contient donc beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
33ja4h0xfkp4kn4ug6szdengf3q05td
763211
763210
2026-04-07T18:44:39Z
Mewtow
31375
/* Les autres circuits, et résumé */
763211
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==La gestion des dépendances de données==
Un processeur de ''shaders'' SIMD contient donc beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
66lb5neciz2f6docooqy3hq7xgcv3zr
763212
763211
2026-04-07T18:45:23Z
Mewtow
31375
/* La gestion des dépendances de données */
763212
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==La gestion des dépendances de données==
Un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
tsnolrv6ztx8rwi2satfpp264dce1no
763213
763212
2026-04-07T19:04:51Z
Mewtow
31375
/* La gestion des dépendances de données */
763213
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==La gestion des dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d'''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d'''issue'' peut bloquer l'exécution d'une instruction si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du pipeline. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d'Issue analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances. Cela permet d'exécuetr plusieurs instructions en même temps, à des étapes différentes.
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente.
Un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est en théorie possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire, en même temps, un calcul dans chaque ALU. Pendant que l'unité transcendantale fait un calcul trigonométrique quelconque, l'autre ALU effectue des calculs SIMD sur d'autres données.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté. Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
97bow4rz407d4qoivvygen3bqdxlnth
763214
763213
2026-04-07T19:10:38Z
Mewtow
31375
/* La gestion des dépendances de données */
763214
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==La gestion des dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d'''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d'''issue'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d'Issue analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances. Cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème.
L'unité d'''issue'' gère ce genre de problèmes. Elles détecte les dépendances entre instructions et les gère sans intervention extérieure. Elle est très simple et se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. L'unité d'''issue'' ne fait pas comme sur les CPU, qui disposent de techniques d'exécution dans le désordre pour éviter cela.
===Pourquoi plusieurs unités de calcul ?===
Un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté.
Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
2hw9lycwgxeqqmkzesv8bl8yqsf5448
763215
763214
2026-04-07T19:11:20Z
Mewtow
31375
/* La gestion des dépendances de données */
763215
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==La gestion des dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d’''issue'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d’''issue'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances. Cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème.
L'unité d’''issue'' gère ce genre de problèmes. Elles détecte les dépendances entre instructions et les gère sans intervention extérieure. Elle est très simple et se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. L'unité d’''issue'' ne fait pas comme sur les CPU, qui disposent de techniques d'exécution dans le désordre pour éviter cela.
===Pourquoi plusieurs unités de calcul ?===
Un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, qui peuvent en théorie fonctionner en parallèle. Il est possible d'exécuter des instructions séparés dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale.
Pour cela, une possibilité est d'utiliser des instructions à ''co-issue''. Le problème est que ces instructions sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Aussi, nous allons mettre la ''co-issue'' de côté.
Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Les processeurs de ''shaders'' en sont capables, tout comme les CPU. Mais les CPU utilisent au mieux cette possibilité. Ils intègrent des circuits d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
tp4zl5sq30ts0abayn3i0dr1q4yxbpk
763216
763215
2026-04-07T19:18:07Z
Mewtow
31375
/* La gestion des dépendances de données */
763216
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d’''issue'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d’''issue'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, identiques ou non. Cela permet d'exécuter des instructions consécutives dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale.
Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème.
L'unité d’''issue'' gère ce genre de problèmes. Elles détecte les dépendances entre instructions et les gère sans intervention extérieure. Elle est très simple et se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Mais les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
qzmso08bt0jlxfg48vooosjes9uc7st
763217
763216
2026-04-07T19:20:51Z
Mewtow
31375
/* Les autres circuits, et résumé */
763217
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d’''issue'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d’''issue'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unité de calcul, identiques ou non. Cela permet d'exécuter des instructions consécutives dans des unités de calcul séparées. Par exemple, reprenons l'exemple de l'unité de vertices de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale.
Dans ce qui suit, nous allons partir du principe que le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout d’exécuter plusieurs instructions en même temps dans des unités de calcul séparées. La raison est que la plupart des instructions prend plusieurs cycles d'horloge à s’exécuter. Ainsi, pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, s'il y en a une. La seule contrainte est que les deux instructions soient indépendantes.
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème.
L'unité d’''issue'' gère ce genre de problèmes. Elles détecte les dépendances entre instructions et les gère sans intervention extérieure. Elle est très simple et se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Mais les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
96un474tx0mf7smrdimaebz22v8nx0p
763218
763217
2026-04-07T19:26:13Z
Mewtow
31375
/* Le pipeline du processeur */
763218
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'' qui détermine si l’opération peut s'exécuter, la fait attendre si ce n'est pas le cas, et configure le banc de registres.
L'unité d’''issue'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible ou que les opérandes sont en cours de calcul. Elle est très importante, car elle intègre de nombreuses optimisations permettant de profiter du pipeline. Elle est aussi appelée le ''scoreboard''.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* l'unité d’''issue'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. L'unité d’''issue'' gère ce genre de problèmes. Elles détecte les dépendances entre instructions et les gère sans intervention extérieure. Elle est très simple et se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée.
Les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions. Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Et cs derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour remplir ces temps d'attente avec des calculs indépendants de la texture lue.
Les anciennes cartes graphiques d'avant les années 2000 géraient une forme particulière de '''lectures non-bloquantes'''. L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Il ne faut pas qu'il y a ai une dépendance entre les instructions exécutées dans l'ALU et un texel pas encore lu. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le processeur de ''shader'' incorpore un circuit situé juste après le décodeur d'instruction, appelé l'unité d'émission. Elle compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer le cas des instructions d'accès mémoire. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des ALU différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en parallèle dans des unités de calcul séparées. Sur les cartes graphiques des années 2000-2010, un circuit dédié appelé le '''''scoreboard''''' détectait ces dépendances. Pour cela, il vérifie si deux instructions utilisent les mêmes registres.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une. Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premire mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
Il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ldlafg02m1v3czjwdk675zzlxmn7gbb
763220
763218
2026-04-07T19:45:29Z
Mewtow
31375
/* L'unité de contrôle et les dépendances de données */
763220
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Par exemple, si aucune unité de calcul n'est disponible, ou que les opérandes sont en cours de calcul. Il se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
===Les lectures non-bloquantes===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes'''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
===Le ''scoreboard'' des GPU des années 2000-2010===
Les lectures non-bloquantes permettent de gérer les lectures. Mais il y a d'autres instructions qui prennent plus d'un cycle d'horloge pour s'exécuter. Les instructions de calcul basiques d'un GPU prennent 2 à 3 cycles d'horloge dans le meilleur des cas, parfois plus d'une dizaine. De telles instructions sont autant d'opportunités pour exécuter plusieurs instructions en même temps. Pendant qu'une instruction multicycle occupe une ALU, les autres ALU peuvent accepter d'autres instructions.
Je rappelle que les processeurs de ''shaders'' SIMD peuvent démarrer une nouvelle instruction scalaire/SIMD par cycle, si les conditions sont réunies. Le problème est alors le suivant : deux instructions démarrées l'une après l'autre doivent occuper des unités de calcul différentes, mais en plus elles doivent manipuler des données différentes.
Prenons un exemple simple : une instruction calcule un résultat, qui est utilisé comme opérande par une seconde instruction. La seconde ne doit pas démarrer tant que la première n'a pas enregistré son résultat dans les registres. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. Il existe aussi d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres, mais laissons-les de côté pour le moment. Il faut dire qu'avec 4096 registres par ''shader'', elles sont plus rares. Les dépendances en question sont regroupées sous le terme de '''dépendances de données'''.
S'il y a une dépendance de données entre deux instructions, on ne peut pas les exécuter en même temps, la première doit se terminer avant de démarrer la seconde. Le ''scoreboard'' détecte ces dépendances, en vérifiant si deux instructions utilisent les mêmes registres. Il vérifie aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
: Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
kw6s10j4mo7ov3hvkiiuthrvw2oolla
763224
763220
2026-04-07T20:01:46Z
Mewtow
31375
/* L'unité de contrôle et les dépendances de données */
763224
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Mais cela pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
===Les lectures non-bloquantes===
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes'''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
drsuxjx9s052ik73gjssyxbopd36rhs
763225
763224
2026-04-07T20:02:08Z
Mewtow
31375
/* Le scoreboard des GPU des années 2000-2010 */
763225
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
===Les lectures non-bloquantes===
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes'''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
imr6u3s2urjbqiqzke7aqrr21xifxbn
763226
763225
2026-04-07T20:03:25Z
Mewtow
31375
/* Les lectures non-bloquantes */
763226
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
022w0bwwgi1f8g56kqmb7yvlw0ajj95
763227
763226
2026-04-07T20:03:36Z
Mewtow
31375
/* Les lectures non-bloquantes avec multithreading matériel */
763227
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes'''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
euu382z9nnowgp2d85eiccol5ycmetm
763228
763227
2026-04-07T20:04:44Z
Mewtow
31375
/* Les lectures non-bloquantes avec multithreading matériel */
763228
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
n65vs6f01zfbet9h6l5v16woiospeqn
763229
763228
2026-04-07T20:05:02Z
Mewtow
31375
/* Le scoreboard des GPU des années 2000-2010 */
763229
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que si un ''thread'' effectue un accès mémoire, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
5ggw06yjv7pfyf7qiuwo9mk9pzwkzas
763230
763229
2026-04-07T20:07:02Z
Mewtow
31375
/* Les lectures non-bloquantes avec multithreading matériel */
763230
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncraties. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des opérations arithmétiques et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception, et incorporent ces circuits. Ce qu'il y a dans les processeurs de shaders, leur microarchitecture, est cependant un peu plus simple que sur les CPU. On n'y retrouve pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. En conséquence, l'unité de contrôle est simple, peu complexe. La majeure partie du processeur est dédié aux unités de calcul.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', une technique qui vient du monde des processeurs, où cette technique a été appliquée à de nombreuses reprises. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un ''thread'' est bloqué par un accès mémoire, d'autres ''threads'' exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire. Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de ''shaders'' en données. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on effectue une lecture dans l'unité de texture, on peut faire des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'ont pas comme opérande un texel en cours de lecture. Pour cela, la carte graphique fait des vérifications sur les registres. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées.
Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de faire des comparaisons. Pour cela, le ''scoreboard'' compare les registres utilisés par l'instruction avec le registre de destination. Si il y a correspondance, l'instruction est mise en attente. La mise en attente bloque alors toutes les instructions suivantes. En clair, dès qu'on tombe sur une instruction dépendante, le processeur cesse d'émettre des instructions dans l'ALU, et attend la fin de la lecture. Il n'a pas vraiment le choix. Faire autrement serait possible, mais demanderait d'implémenter des techniques d'exécution dans le désordre très gourmandes en circuits.
Depuis l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. L'idée est que le processeur exécute des instructions en parallèle d'une lecture. Mais si une dépendance survient avec la lecture, le ''thread'' est mis en pause pendant l'accès mémoire et laisse la place à un autre. Le ''thread'' est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Ou encore, un ''thread'' aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'avantage est que si un thread est mis en pause par une lecture, les autres threads récupèrent les cycles d'horloge du thread mis en pause. Par exemple, sur un processeur qui gère 16 threads concurrents, si l'un d'eux est mis en pause, le processeur changera de thread tous les 15 cycles au lieu de 16. Ou encore, un thread aura droit à deux cycles consécutifs pour s’exécuter. Faire ainsi marche assez bien si on a beaucoup de threads dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
b0l3p41aaoubgbgr76uti6t9e7478mg
763231
763230
2026-04-07T21:36:26Z
Mewtow
31375
763231
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==L'unité de contrôle et les dépendances de données==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline du processeur===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ifmywofsuqjmqjknb5nhm38ej59g70l
763232
763231
2026-04-07T21:37:02Z
Mewtow
31375
/* L'unité de contrôle et les dépendances de données */
763232
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice. Le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne vaut cependant que pour les instructions dont le compilateur peut prédire la durée. En clair, les accès mémoire et certaines instructions spéciales ne sont pas prises en charge. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a plusieurs par ''thread'', entre 5 et 10 selon le GPU. Une instruction productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices doivent attendre que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même. Le masque est composé de bits à 0/1, un par compteur.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
cj5unuxqwl9ar5nsufcafe881hg9l1o
763233
763232
2026-04-07T21:40:29Z
Mewtow
31375
/* L'encodage explicite des dépendances sur les GPU post-2010 */
763233
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
A chaque cycle, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, mais aussi d'autres formes de dépendances. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction.S'il n'y a pas de dépendance, elle est envoyée à une unité de calcul inoccupée, s'il y en a une.
Mais s'il y a une dépendance de donnée, le processeur de ''shader'' met en pause le ''thread''/''warp'' et bascule sur un autre ''thread''. Le ''thread'' est mis en pause tant que la dépendance n'est pas résolue. A chaque cycle, le processeur détecte les dépendances et choisit une instruction parmi celles qui peuvent s'exécuter, peu importe leur ''thread''.
L'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions SIMD. Les instructions SIMD sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. Les instructions attendent leur tour dans la file d'instruction. L'unité d'émission vérifie à chaque cycle quelles instructions sont prêtes à s'exécuter, dans chaque file d'instruction. L'unité d'émission choisit un ''thread'' et envoie l'instruction la plus ancienne dans la file d'instruction aux unités de calcul. Les instructions qui ont besoin d'un résultat pas encore calculé ou lu depuis la mémoire attendent juste.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Mais pour cela, il faut que ces derniers aient des instructions en attente dans la file d'instruction. Le processeur doit avoir chargé assez d'instructions en avance, il faut que chaque ''thread'' ait assez de réserve pour exécuter plusieurs instructions consécutives. Pour éviter ce problème, le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
pw0y9tcgdqgbowrriszuenotutxfdvv
763234
763233
2026-04-07T21:47:29Z
Mewtow
31375
/* Le scoreboard des GPU des années 2000-2010 */
763234
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
===L'encodage explicite des dépendances avec ''multithreading'' matériel===
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
388lryhjmvofvemht3eh7r26adjzsz3
763235
763234
2026-04-07T21:47:47Z
Mewtow
31375
/* L'encodage explicite des dépendances avec multithreading matériel */
763235
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
10fbocvjbg3u4tevcfdmk7awlr00pm8
763236
763235
2026-04-07T22:49:39Z
Mewtow
31375
/* Le scoreboard des GPU des années 2000-2010 */
763236
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Les lectures non-bloquantes avec ''multithreading'' matériel===
Les processeurs de ''shader'' sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Fort heureusement, les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture.
L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
L'implémentation de cette technique est assez simple au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
a80uj7kou6zambt8m9qqfeetcu5g6og
763237
763236
2026-04-07T23:03:29Z
Mewtow
31375
/* Le multithreading matériel des processeurs de shaders */
763237
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel'', appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread''/''warp'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles.
Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail. Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''.
Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''. Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Par contre, avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
eo6dlvinhx29kxjge54l4ua4hy791pv
763238
763237
2026-04-07T23:03:40Z
Mewtow
31375
/* Les lectures non-bloquantes avec le Fine Grained Multithreading */
763238
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel'', appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread''/''warp'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles.
Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail. Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''.
Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Par contre, avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
oj3dgilktgt2cozmj42t7ebsvws6meq
763239
763238
2026-04-07T23:06:24Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763239
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel'', appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread''/''warp'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Par contre, avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
gocrshymm51u8y2fm5cf157qhv06ioi
763240
763239
2026-04-07T23:09:36Z
Mewtow
31375
/* Les lectures non-bloquantes avec le Fine Grained Multithreading */
763240
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel'', appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread''/''warp'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''scoreboard'' des GPU des années 2000-2010===
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. S'il y a un problème, le processeur de ''shader'' met en pause le ''thread''/''warp'', l'instruction est bloquée, de même que les suivantes. Par contre, avec le ''multithreading'' matériel, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
8wtx9n6ayc8ud9f0zbamakz1bi6vj0d
763241
763240
2026-04-07T23:12:56Z
Mewtow
31375
/* Le scoreboard des GPU des années 2000-2010 */
763241
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel'', appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread''/''warp'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
kj7s3n94onep2g575zdoucjwfuijwul
763242
763241
2026-04-07T23:13:33Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763242
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
b65pt4c6vytj8ay5q94fyajantabnch
763243
763242
2026-04-07T23:15:09Z
Mewtow
31375
/* Le Fine Grained Multithreading */
763243
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
0m1nenpimqwzrbzp86617ekc9v5r7h8
763244
763243
2026-04-07T23:15:34Z
Mewtow
31375
/* Le Fine Grained Multithreading avec un scoreboard élaboré */
763244
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur est composé de quatre circuits principaux :
* des unités de calcul, qui font des calculs et d'autres instructions ;
* des registres pour mémoriser les opérandes des calculs ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions/opérations.
Les processeurs de shader ne font pas exception et incorporent ces circuits. Par contre, leur unité de contrôle est très simple, car on n'utilise pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
==Les unités de calcul d'un processeur de shader SIMD==
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, non-regroupés dans un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
Et ces différentes instructions ont toutes des unités de calcul dédiées, ce qui fait qu'un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. Mais d'autres unités de calcul sont présentes, surtout sur les architectures modernes. Il n'est pas rare de trouver une unité de calcul entière, ou des unités de calcul flottantes spécialisées pour les opérations transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
===Les autres circuits, et résumé===
En plus des unités de calcul, un processeur de shader contient d'autres circuits. Les registres sont spécialisés, à savoir que les registres SIMD sont séparés des registres scalaires. Les registres sont regroupés dans des '''bancs de registres''', qui sont de petites mémoires dont chaque adresse contient un registre. Il y a des bancs de registres séparés pour les scalaires et les vecteurs SIMD.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
Le tout est résumé dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, ici appelé l'''ultra threaded dispatcher''. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU). Ils partagent un cache L2 ou L3, ici un cache L2. Le GPU contient aussi des contrôleurs mémoire, qui lit ou écrit des données en mémoire vidéo. Les processeurs de shaders sont reliés par un réseau d'interconnexion plus ou moins complexe, qui n'est pas détaillé ici.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
L'unité de contrôle est représentée comme un bloc monolithique, mais elle est en réalité composée de plusieurs circuits qui s'enchainent l'un à la suite de l'autre. Et nous allons détailler cela dans la section suivante.
==Le pipeline d'un processeur de shader et son unité de contrôle==
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
A cela, il faut rajouter les bancs de registres, les unités de calcul entières (ALU), les unités de calcul flottantes/SIMD (FPU), une unité mémoire (LSU) et parfois une unité pour les opérations complexes (SFU). Il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants, qu'on ne peut pas détailler ici. Le tout est illustré ci-dessous, mais ne faites pas attention aux détails de ce graphique, tout sera expliqué" en temps et en heure.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' des GPU des années 2000-2010===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteur, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regarde l'un ou l'autre des compteurs selon leur situation.
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistors conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
===Le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Les tout premiers GPU utilisaient vraisemblablement une forme très basique de ''multithreading'' matériel, appelée le ''Fine Grained Multithreading''. L'idée est que le processeur de shader change de ''thread'' à chaque cycle d'horloge. Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut même se passer de ''scoreboard'', ou du moins se limiter avec un ''scoreboard'' très limité.
Un problème avec cette technique est lié aux accès mémoire. Pour rappel, la mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long. Environ une centaine de cycles d'horloge, si celle-ci est lue depuis la mémoire vidéo. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais ça ne fait pas de miracles. Toujours est-il que quand on lance un accès mémoire, on ne sait pas combien de temps l'accès mémoire va durer. Il peut facilement durer quelques centaines de cycles d'horloge, à une dizaine de cycles si le cache fait son travail.
Un moyen très simple de gérer cela avec le ''Fine Grained Multithreading'', est de mettre le ''thread'' en pause. Le ''thread'' est mis en pause, même si d'autres instructions pourraient être lancées à sa suite. Et il redémarre quand l'accès mémoire est terminé. Il y a d'autres moyens de faire, mais ils demandent de modifier le ''scoreboard''. Le résultat est qu'Un ''thread'' peut être en cours d'exécution, mais il peut aussi être mis en pause. Le processeur doit donc savoir quels ''threads'' sont en cours d'exécution et en pause. Le processeur doit donc avoir une table pour mémoriser l'état de chaque ''thread''.
L'implémentation de cette technique est assez simple, au premier abord. L'unité de chargement a plusieurs ''program counter'', un par ''thread''. Un circuit dédié sait quels ''threads'' sont mis en pause, et en envoie un aux unités de calculs. La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
===Les lectures non-bloquantes avec le ''Fine Grained Multithreading''===
Les processeurs de ''shaders'' disposent d'optimisations pour continuer à faire des calculs pendant un accès à une texture. L'idée est simple : pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU de calcul. La technique porte le nom de '''lectures non-bloquantes''' et elle surgit naturellement quand on utilise un ''scoreboard''.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas le texel en cours de lecture. Heureusement, le ''scoreboard'' gère le cas. La lecture charge le texel dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais les autres ont une dépendance et ne sont pas exécutées, le ''scoreboard'' les bloque. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Depuis au moins l'époque de Direct X 9, les GPU combinent les lectures non-bloquantes avec le ''multithreading'' matériel. La différence tient dans le moment où un ''thread'' est mis en pause. Sans lectures non-bloquantes, on change de ''thread'' dès qu'un accès mémoire démarre. Mais avec les lectures non-bloquantes, le ''thread'' n'est pas immédiatement stoppé. A la place, le processeur continue à exécuter des instructions de calcul, indépendantes de la lecture. Par contre, dès qu'une instruction a besoin de la donnée lue, le ''thread'' est mis en pause. La mise en pause des ''threads'' a donc lieu plus tard, elle est retardée.
Si une dépendance survient avec la lecture, le ''thread'' est mis en pause le temps que la lecture se termine. Mais il laisse la place à un autre ''thread'', qui pourra exécuter des calculs. Le ''thread'' mis en pause est réveillé une fois que l'accès mémoire est terminé. L'avantage est que si un ''thread'' est mis en pause par une lecture, les autres ''threads'' récupèrent les cycles d'horloge du ''thread'' mis en pause. Par exemple, sur un processeur qui gère 16 ''threads'' concurrents, si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Faire ainsi marche assez bien si on a beaucoup de ''threads'' dont peu d'entre eux font des lectures/écritures.
===Le ''Fine Grained Multithreading'' avec un ''scoreboard'' élaboré===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il peut exécuter plusieurs instructions consécutives ,d'un même ''thread'', dans des cycles d'horloge consécutifs. Il change de ''thread'' plus rarement, seulement quand les conditions l'imposent. Typiquement, ils ne changent de ''thread'' que si le ''thread'' est bloqué en raison d'une dépendance quelconque.
Pour rappel, le ''scoreboard'' reçoit une instruction provenant du décodeur et vérifie si elle peut être exécutée. Il vérifie les dépendances de données, s'il y a une unité de calcul libre pour exécuter l'instruction, et bien d'autres choses. Si l'instruction ne poeut pas démarrer, quelle qu'en soit la raison, le processeur de ''shader'' met en pause le ''thread''/''warp''. L'instruction est bloquée, de même que les suivantes. Par contre, le processeur n'est cependant pas complétement bloqué. A la place, il bascule sur un autre ''thread''.
Pour cela, l'implémentation sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisit.
Plus haut, j'ai dit que les instructions disaient combien de temps elles duraient, la durée étant intégrée dans l'instruction. Mais je dois compléter : il arrive même que le switch de ''thread'' soit géré par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
De nos jours, le ''scoreboard'' peut émettre deux instructions appartenant à des ''threads''/''warps'' différents lors du même cycle d'horloge. Il s'agit d'une possibilité d'émission multiple, qui est cependant soumise à conditions. Il faut que les deux instructions utilisent des unités de calcul différentes. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières n'étaient pas présentes. De plus, le faible budget en transistor faisait que l'on ajoutait pas d'unité flottante scalaire, ce qui ne servait pas à grand-chose. Par contre, l'unité de calcul transcendantale était systématiquement présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
fuccumc4jcm2lyuxkrhbs2ghj2jy6tq
Mathc initiation/005h
0
83698
763219
763105
2026-04-07T19:41:15Z
Xhungab
23827
763219
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 (forme explicite réduite)]]}}
{{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}}
12lyaixhl9yeag92q0x24jfmmvdwpcp
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter
2
83748
763131
763037
2026-04-07T15:57:04Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter]] sans laisser de redirection : Contenu inédit : protologismes
763037
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
{{Autres projets|w=Mot rare}}Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture''' ;
* '''[[Guide des mots rares à adopter/culicellique|culicellique]]''' : qui rappelle par sa finesse, sa légèreté ou sa délicatesse '''l’aspect ou le comportement d’un tout petit insecte volant''' ;
* '''[[Guide des mots rares à adopter/imbrigène|imbrigène]]''' : qui est '''né de la pluie ou a été façonné par les gouttes d’eau''' ;
* [[Guide des mots rares à adopter/fâtiloque|'''fâtiloque''']] : qui '''prédit l’avenir ou connaît le destin''' ;
* '''[[Guide des mots rares à adopter/fatilégue|fatilégue]]''' : qui '''récolte les âmes ou collectionne les morts'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
16dq0nix5x3jjfiff9zvxihe42fvlw3
763151
763131
2026-04-07T15:57:24Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] sans laisser de redirection
763037
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
{{Autres projets|w=Mot rare}}Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture''' ;
* '''[[Guide des mots rares à adopter/culicellique|culicellique]]''' : qui rappelle par sa finesse, sa légèreté ou sa délicatesse '''l’aspect ou le comportement d’un tout petit insecte volant''' ;
* '''[[Guide des mots rares à adopter/imbrigène|imbrigène]]''' : qui est '''né de la pluie ou a été façonné par les gouttes d’eau''' ;
* [[Guide des mots rares à adopter/fâtiloque|'''fâtiloque''']] : qui '''prédit l’avenir ou connaît le destin''' ;
* '''[[Guide des mots rares à adopter/fatilégue|fatilégue]]''' : qui '''récolte les âmes ou collectionne les morts'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
16dq0nix5x3jjfiff9zvxihe42fvlw3
763171
763151
2026-04-07T16:00:22Z
JackPotte
5426
sortie des moteurs de recherche
763171
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
{{Autres projets|w=Mot rare}}Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture''' ;
* '''[[Guide des mots rares à adopter/culicellique|culicellique]]''' : qui rappelle par sa finesse, sa légèreté ou sa délicatesse '''l’aspect ou le comportement d’un tout petit insecte volant''' ;
* '''[[Guide des mots rares à adopter/imbrigène|imbrigène]]''' : qui est '''né de la pluie ou a été façonné par les gouttes d’eau''' ;
* [[Guide des mots rares à adopter/fâtiloque|'''fâtiloque''']] : qui '''prédit l’avenir ou connaît le destin''' ;
* '''[[Guide des mots rares à adopter/fatilégue|fatilégue]]''' : qui '''récolte les âmes ou collectionne les morts'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />
[[Catégorie:Guide des mots rares à adopter]]
k2fmo68t1aypus46wo9dy6g6hw6klep
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/crépat, crépate
2
83749
763145
763015
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/crépat, crépate]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/crépat, crépate]] sans laisser de redirection : Contenu inédit : protologismes
763015
wikitext
text/x-wiki
== crépat, crépate ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:bruyant|bruyant]] mais [[wikt:envoûtant|envoûtant]]''', capable de captiver l’attention par son [[wikt:intensité|intensité]] [[wikt:harmonieuse|harmonieuse]] ou [[wikt:rythmique|rythmique]]. Le terme ''crépat'' évoque une sonorité [[wikt:vivante|vivante]], [[wikt:vibrante|vibrante]], presque [[wikt:hypnotique|hypnotique]], dont le bruit devient source de [[wikt:fascination|fascination]] plutôt que de [[wikt:gêne|gêne]]<ref>https://limiel.omeka.net/items/show/43</ref>.
''Exemple :'' La musique classique est crépate à mon égard.
=== Antonymie : crépat vs clangart ===
Le mot ''crépat'' s’oppose directement à ''[[Guide des mots rares à adopter/clangart, clangarte|clangart]]'', adjectif qui qualifie une sonorité '''soudainement assourdissante''', souvent perçue comme agressive ou perturbatrice. Là où ''crépat'' suggère un bruit maîtrisé, séduisant et immersif, ''clangart''renvoie à une rupture sonore brutale qui altère l’ambiance.
Ainsi, une ambiance sonore peut être dite ''crépate'' lorsqu’elle charme et enveloppe, tandis qu’elle devient ''clangarte''lorsqu’elle dérange ou rompt l’harmonie.
=== Étymologie ===
Du latin ''crepare'' (« faire du bruit, craquer »), ici réinterprété dans une perspective positive, où le bruit devient source d’émotion esthétique.
=== Source ===
[https://limiel.omeka.net/items/show/43 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
o7pw89n3hui3ouacc3z9zfe2h8qgczq
763165
763145
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/crépat, crépate]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/crépat, crépate]] sans laisser de redirection
763015
wikitext
text/x-wiki
== crépat, crépate ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:bruyant|bruyant]] mais [[wikt:envoûtant|envoûtant]]''', capable de captiver l’attention par son [[wikt:intensité|intensité]] [[wikt:harmonieuse|harmonieuse]] ou [[wikt:rythmique|rythmique]]. Le terme ''crépat'' évoque une sonorité [[wikt:vivante|vivante]], [[wikt:vibrante|vibrante]], presque [[wikt:hypnotique|hypnotique]], dont le bruit devient source de [[wikt:fascination|fascination]] plutôt que de [[wikt:gêne|gêne]]<ref>https://limiel.omeka.net/items/show/43</ref>.
''Exemple :'' La musique classique est crépate à mon égard.
=== Antonymie : crépat vs clangart ===
Le mot ''crépat'' s’oppose directement à ''[[Guide des mots rares à adopter/clangart, clangarte|clangart]]'', adjectif qui qualifie une sonorité '''soudainement assourdissante''', souvent perçue comme agressive ou perturbatrice. Là où ''crépat'' suggère un bruit maîtrisé, séduisant et immersif, ''clangart''renvoie à une rupture sonore brutale qui altère l’ambiance.
Ainsi, une ambiance sonore peut être dite ''crépate'' lorsqu’elle charme et enveloppe, tandis qu’elle devient ''clangarte''lorsqu’elle dérange ou rompt l’harmonie.
=== Étymologie ===
Du latin ''crepare'' (« faire du bruit, craquer »), ici réinterprété dans une perspective positive, où le bruit devient source d’émotion esthétique.
=== Source ===
[https://limiel.omeka.net/items/show/43 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
o7pw89n3hui3ouacc3z9zfe2h8qgczq
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/Introduction
2
83750
763132
763012
2026-04-07T15:57:04Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/Introduction]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/Introduction]] sans laisser de redirection : Contenu inédit : protologismes
763012
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter#Sommaire|pageText=Retour au sommaire}}
== Introduction ==
[[Français|La langue française]] est un territoire vaste, parfois familier, parfois mystérieux — une véritable ''[[Guide des mots rares à adopter/caullée|caullée]]'' de mots, faite d’aspérités, de creux oubliés et de sommets encore inexplorés. Au fil du temps, certains termes se sont imposés dans l’usage courant, tandis que d’autres, [[w:Mot_rare|plus rares]], plus discrets, ont peu à peu glissé dans l’ombre. Pourtant, ce sont souvent ces mots oubliés ou méconnus qui portent en eux une richesse singulière, une précision et une poésie que le langage quotidien ne suffit pas toujours à exprimer.
Ce guide propose justement de partir à leur rencontre. Il invite le lecteur à ''exquirer'' la langue, à l’examiner avec attention, [[wikt:curiosité|curiosité]] et sens du détail, afin d’en révéler les nuances les plus fines. Car enrichir son [[Français/Littérature|vocabulaire]] ne consiste pas seulement à accumuler des mots, mais à affiner sa manière de penser, de ressentir et de dire le monde.
À travers ces pages, vous découvrirez des termes [[w:Mot_rare|rares]], parfois anciens, parfois [[w:Néologisme|nouvellement formés]], tous choisis pour leur capacité à élargir l’expression et à éveiller l’imaginaire. Chacun d’eux est une porte entrouverte sur une idée, une sensation ou une image plus précise, plus nuancée, plus vivante.
Explorer les mots rares, c’est aussi redonner une voix à ce qui semblait perdu, et réinvestir un patrimoine linguistique en constante évolution. Que vous soyez [[w:Écrivain|écrivain]], étudiant, passionné de langue ou simple curieux, ce [[wikt:livre|livre]] vous propose une invitation : celle de redécouvrir le plaisir des mots, et peut-être, de faire vôtres ceux que l’on n’entend presque plus.
<small>< [[Guide des mots rares à adopter#Sommaire|Accéder au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
6fymnml3rxjidx4cwr53yuzxg4xy5pu
763152
763132
2026-04-07T15:57:24Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/Introduction]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/Introduction]] sans laisser de redirection
763012
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter#Sommaire|pageText=Retour au sommaire}}
== Introduction ==
[[Français|La langue française]] est un territoire vaste, parfois familier, parfois mystérieux — une véritable ''[[Guide des mots rares à adopter/caullée|caullée]]'' de mots, faite d’aspérités, de creux oubliés et de sommets encore inexplorés. Au fil du temps, certains termes se sont imposés dans l’usage courant, tandis que d’autres, [[w:Mot_rare|plus rares]], plus discrets, ont peu à peu glissé dans l’ombre. Pourtant, ce sont souvent ces mots oubliés ou méconnus qui portent en eux une richesse singulière, une précision et une poésie que le langage quotidien ne suffit pas toujours à exprimer.
Ce guide propose justement de partir à leur rencontre. Il invite le lecteur à ''exquirer'' la langue, à l’examiner avec attention, [[wikt:curiosité|curiosité]] et sens du détail, afin d’en révéler les nuances les plus fines. Car enrichir son [[Français/Littérature|vocabulaire]] ne consiste pas seulement à accumuler des mots, mais à affiner sa manière de penser, de ressentir et de dire le monde.
À travers ces pages, vous découvrirez des termes [[w:Mot_rare|rares]], parfois anciens, parfois [[w:Néologisme|nouvellement formés]], tous choisis pour leur capacité à élargir l’expression et à éveiller l’imaginaire. Chacun d’eux est une porte entrouverte sur une idée, une sensation ou une image plus précise, plus nuancée, plus vivante.
Explorer les mots rares, c’est aussi redonner une voix à ce qui semblait perdu, et réinvestir un patrimoine linguistique en constante évolution. Que vous soyez [[w:Écrivain|écrivain]], étudiant, passionné de langue ou simple curieux, ce [[wikt:livre|livre]] vous propose une invitation : celle de redécouvrir le plaisir des mots, et peut-être, de faire vôtres ceux que l’on n’entend presque plus.
<small>< [[Guide des mots rares à adopter#Sommaire|Accéder au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
6fymnml3rxjidx4cwr53yuzxg4xy5pu
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clangart, clangarte
2
83751
763142
763016
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/clangart, clangarte]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/clangart, clangarte]] sans laisser de redirection : Contenu inédit : protologismes
763016
wikitext
text/x-wiki
== clangart, clangarte ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:soudainement|soudainement]] [[wikt:assourdissant|assourdissant]]''', souvent perçu comme [[wikt:agressif|agressif]], [[wikt:brutal|brutal]] ou [[wikt:désagréable|désagréable]]. Le terme ''clangart'' évoque une rupture sonore nette, un bruit qui surgit et perturbe une ambiance jusque-là paisible ou harmonieuse<ref>https://limiel.omeka.net/items/show/62</ref>.
''Exemple :'' Une sonorité clangarte a interrompu le concert.
=== Antonymie : clangart vs crépat ===
Le mot ''clangart'' s’oppose directement à ''[[Guide des mots rares à adopter/crépat, crépate|crépat]]'', adjectif qui qualifie une sonorité '''bruyante mais envoûtante''', capable de séduire malgré son intensité. Là où ''clangart'' renvoie à un bruit dérangeant et désagréable, ''crépat'' suggère au contraire une expérience sonore immersive et captivante.
Ainsi, une ambiance sonore est dite ''clangarte'' lorsqu’elle devient intrusive et désagréable, tandis qu’elle est ''crépate''lorsqu’elle charme et retient l’attention.
=== Étymologie ===
Du latin ''clangor'' (« son éclatant, bruit retentissant »), dont le sens est ici accentué pour souligner le caractère abrupt et perturbateur du son.
=== Source ===
[https://limiel.omeka.net/items/show/62 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
p0t4sd7fdvp87lb5vsvn23bnmvdb0am
763162
763142
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/clangart, clangarte]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clangart, clangarte]] sans laisser de redirection
763016
wikitext
text/x-wiki
== clangart, clangarte ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:soudainement|soudainement]] [[wikt:assourdissant|assourdissant]]''', souvent perçu comme [[wikt:agressif|agressif]], [[wikt:brutal|brutal]] ou [[wikt:désagréable|désagréable]]. Le terme ''clangart'' évoque une rupture sonore nette, un bruit qui surgit et perturbe une ambiance jusque-là paisible ou harmonieuse<ref>https://limiel.omeka.net/items/show/62</ref>.
''Exemple :'' Une sonorité clangarte a interrompu le concert.
=== Antonymie : clangart vs crépat ===
Le mot ''clangart'' s’oppose directement à ''[[Guide des mots rares à adopter/crépat, crépate|crépat]]'', adjectif qui qualifie une sonorité '''bruyante mais envoûtante''', capable de séduire malgré son intensité. Là où ''clangart'' renvoie à un bruit dérangeant et désagréable, ''crépat'' suggère au contraire une expérience sonore immersive et captivante.
Ainsi, une ambiance sonore est dite ''clangarte'' lorsqu’elle devient intrusive et désagréable, tandis qu’elle est ''crépate''lorsqu’elle charme et retient l’attention.
=== Étymologie ===
Du latin ''clangor'' (« son éclatant, bruit retentissant »), dont le sens est ici accentué pour souligner le caractère abrupt et perturbateur du son.
=== Source ===
[https://limiel.omeka.net/items/show/62 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
p0t4sd7fdvp87lb5vsvn23bnmvdb0am
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/aconcordesque
2
83752
763133
763018
2026-04-07T15:57:05Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/aconcordesque]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/aconcordesque]] sans laisser de redirection : Contenu inédit : protologismes
763018
wikitext
text/x-wiki
== aconcordesque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur. Par extension, ''aconcordesque'' qualifie une attitude, une décision ou un comportement qui '''s’oppose aux désirs amoureux''' ou aux affinités naturelles entre deux êtres<ref>https://limiel.omeka.net/items/show/13</ref>.
''Exemple :'' Elle l’eut laissé de manière aconcordesque pour répondre aux dogmes de l’orthodoxie.
=== Champ sémantique et usage ===
Le terme ''aconcordesque'' appartient au champ des dissonances affectives et des ruptures sentimentales. Il permet de décrire avec finesse ces moments où les sentiments, bien que présents, sont contredits par des choix, des contraintes ou des principes extérieurs.
Une décision est dite ''aconcordesque'' lorsqu’elle va à l’encontre de l’amour ressenti, brisant une harmonie émotionnelle pourtant évidente.
=== Intérêt linguistique ===
Le mot ''aconcordesque'' revêt un intérêt particulier pour la langue française en ce qu’il permet d’exprimer, en un seul terme, une réalité affective complexe : '''le conflit entre amour et contrainte'''. Là où le français courant nécessite souvent des formulations longues (''agir contre ses sentiments'', ''renoncer à un amour sincère''), ce néologisme offre une alternative concise, élégante et nuancée.
Sa construction hybride, associant le préfixe privatif ''a-'' à la racine latine ''concordia'' (harmonie, accord) et à la terminaison expressive ''-esque,'' renforce son pouvoir évocateur. Il s’inscrit ainsi dans une tradition de mots capables de '''saisir les subtilités de l’expérience amoureuse''', enrichissant le lexique des émotions et des relations humaines.
=== Étymologie ===
Du latin ''concordia'' (« accord, harmonie »), précédé du préfixe grec ''a-'' (privation), et enrichi d’une formation adjectivale en ''-esque''.
=== Source ===
[https://limiel.omeka.net/items/show/13 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 21 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
mlq4mjfdogk94kyncc4nuxapjjs2uyc
763153
763133
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/aconcordesque]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/aconcordesque]] sans laisser de redirection
763018
wikitext
text/x-wiki
== aconcordesque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur. Par extension, ''aconcordesque'' qualifie une attitude, une décision ou un comportement qui '''s’oppose aux désirs amoureux''' ou aux affinités naturelles entre deux êtres<ref>https://limiel.omeka.net/items/show/13</ref>.
''Exemple :'' Elle l’eut laissé de manière aconcordesque pour répondre aux dogmes de l’orthodoxie.
=== Champ sémantique et usage ===
Le terme ''aconcordesque'' appartient au champ des dissonances affectives et des ruptures sentimentales. Il permet de décrire avec finesse ces moments où les sentiments, bien que présents, sont contredits par des choix, des contraintes ou des principes extérieurs.
Une décision est dite ''aconcordesque'' lorsqu’elle va à l’encontre de l’amour ressenti, brisant une harmonie émotionnelle pourtant évidente.
=== Intérêt linguistique ===
Le mot ''aconcordesque'' revêt un intérêt particulier pour la langue française en ce qu’il permet d’exprimer, en un seul terme, une réalité affective complexe : '''le conflit entre amour et contrainte'''. Là où le français courant nécessite souvent des formulations longues (''agir contre ses sentiments'', ''renoncer à un amour sincère''), ce néologisme offre une alternative concise, élégante et nuancée.
Sa construction hybride, associant le préfixe privatif ''a-'' à la racine latine ''concordia'' (harmonie, accord) et à la terminaison expressive ''-esque,'' renforce son pouvoir évocateur. Il s’inscrit ainsi dans une tradition de mots capables de '''saisir les subtilités de l’expérience amoureuse''', enrichissant le lexique des émotions et des relations humaines.
=== Étymologie ===
Du latin ''concordia'' (« accord, harmonie »), précédé du préfixe grec ''a-'' (privation), et enrichi d’une formation adjectivale en ''-esque''.
=== Source ===
[https://limiel.omeka.net/items/show/13 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 21 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
mlq4mjfdogk94kyncc4nuxapjjs2uyc
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/aéruminal, aéruminale
2
83753
763136
763019
2026-04-07T15:57:05Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/aéruminal, aéruminale]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/aéruminal, aéruminale]] sans laisser de redirection : Contenu inédit : protologismes
763019
wikitext
text/x-wiki
== aéruminal, aéruminale ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''accablé de peine, de misères ou de tristesse profonde'''. Par extension, ''aéruminal'' qualifie un cœur, un esprit ou une situation '''saturés de détresses ou de douleurs affectives'''<ref>https://limiel.omeka.net/items/show/93</ref>.
''Exemple :'' Mon cœur si aéruminal.
'''Note d’usage :''' devient ''aéruminaux'' lorsqu’accordé au masculin pluriel.
=== Champ sémantique et usage ===
Le terme ''aéruminal'' s’inscrit dans le champ des émotions intenses et de la souffrance intérieure. Il est particulièrement adapté pour décrire des états de mélancolie, de désespoir ou de peine amoureuse, offrant un mot précis là où le français courant recourt à des périphrases longues.
Une personne ou un cœur est dit ''aéruminal'' lorsqu’il est profondément submergé par la tristesse ou les contrariétés affectives.
=== Intérêt linguistique ===
''aéruminal'' présente un intérêt majeur pour la langue française, car il '''permet d’exprimer en un seul terme la saturation affective par la peine ou la misère'''. Sa formation, dérivée du latin ''aerumnosus'' (« accablé de malheur »), enrichit le vocabulaire des émotions intenses et des états psychologiques profonds.
En littérature ou dans l’écriture poétique, ''aéruminal'' offre une précision émotionnelle rare, capable de rendre en un mot des nuances de douleur, de mélancolie ou de détresse sentimentale que le langage ordinaire peine à transmettre.
=== Étymologie ===
Du latin ''aerumnosus'' (« accablé de malheur, de peine »), francisé pour créer un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/93 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 27 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
ifdoi045gh341jotynhckxviqb98ejo
763156
763136
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/aéruminal, aéruminale]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/aéruminal, aéruminale]] sans laisser de redirection
763019
wikitext
text/x-wiki
== aéruminal, aéruminale ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''accablé de peine, de misères ou de tristesse profonde'''. Par extension, ''aéruminal'' qualifie un cœur, un esprit ou une situation '''saturés de détresses ou de douleurs affectives'''<ref>https://limiel.omeka.net/items/show/93</ref>.
''Exemple :'' Mon cœur si aéruminal.
'''Note d’usage :''' devient ''aéruminaux'' lorsqu’accordé au masculin pluriel.
=== Champ sémantique et usage ===
Le terme ''aéruminal'' s’inscrit dans le champ des émotions intenses et de la souffrance intérieure. Il est particulièrement adapté pour décrire des états de mélancolie, de désespoir ou de peine amoureuse, offrant un mot précis là où le français courant recourt à des périphrases longues.
Une personne ou un cœur est dit ''aéruminal'' lorsqu’il est profondément submergé par la tristesse ou les contrariétés affectives.
=== Intérêt linguistique ===
''aéruminal'' présente un intérêt majeur pour la langue française, car il '''permet d’exprimer en un seul terme la saturation affective par la peine ou la misère'''. Sa formation, dérivée du latin ''aerumnosus'' (« accablé de malheur »), enrichit le vocabulaire des émotions intenses et des états psychologiques profonds.
En littérature ou dans l’écriture poétique, ''aéruminal'' offre une précision émotionnelle rare, capable de rendre en un mot des nuances de douleur, de mélancolie ou de détresse sentimentale que le langage ordinaire peine à transmettre.
=== Étymologie ===
Du latin ''aerumnosus'' (« accablé de malheur, de peine »), francisé pour créer un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/93 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 27 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
ifdoi045gh341jotynhckxviqb98ejo
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/audulard, audularde
2
83754
763134
763020
2026-04-07T15:57:05Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/audulard, audularde]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/audulard, audularde]] sans laisser de redirection : Contenu inédit : protologismes
763020
wikitext
text/x-wiki
== audulard, audularde ==
''Adjectif''
'''Définition :'''
Se dit de quelqu’un qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés. Par extension, ''audulard'' qualifie une personne '''sensible à la cajolerie ou aux louanges superficielles'''<ref>https://limiel.omeka.net/items/show/5</ref>.
''Exemple :'' Une femme très audularde.
=== Champ sémantique et usage ===
Le terme ''audulard'' s’inscrit dans le champ des '''traits de caractère liés à la susceptibilité et à la vanité'''. Il permet de désigner avec précision une personne qui se laisse séduire par les flatteries, sans recourir aux périphrases longues comme ''facilement influençable par les compliments''.
Une attitude est dite ''audularde'' lorsqu’une louange ou une adulation agit directement sur le comportement ou l’enthousiasme d’une personne.
=== Intérêt linguistique ===
Le mot ''audulard'' présente un intérêt particulier pour la langue française car il '''condense en un seul terme une nuance psychologique précise'''. Sa construction, issue du latin ''adulabilis'' (dérivé de ''adulor'', « flatter »), permet de rendre accessible à l’expression littéraire ou analytique la notion de réceptivité aux louanges.
En enrichissant le vocabulaire français, ''audulard'' offre ainsi une alternative élégante aux termes génériques comme ''flatteur'', ''complaisant'' ou ''sensible aux compliments'', tout en apportant une dimension stylistique et nuancée.
=== Étymologie ===
Du latin ''adulabilis'', dérivé de ''adulor'' (« flatter »), francisé pour devenir un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/5 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 15 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
69gxixb199i7s9vd16296d0ksbmdv7h
763154
763134
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/audulard, audularde]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/audulard, audularde]] sans laisser de redirection
763020
wikitext
text/x-wiki
== audulard, audularde ==
''Adjectif''
'''Définition :'''
Se dit de quelqu’un qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés. Par extension, ''audulard'' qualifie une personne '''sensible à la cajolerie ou aux louanges superficielles'''<ref>https://limiel.omeka.net/items/show/5</ref>.
''Exemple :'' Une femme très audularde.
=== Champ sémantique et usage ===
Le terme ''audulard'' s’inscrit dans le champ des '''traits de caractère liés à la susceptibilité et à la vanité'''. Il permet de désigner avec précision une personne qui se laisse séduire par les flatteries, sans recourir aux périphrases longues comme ''facilement influençable par les compliments''.
Une attitude est dite ''audularde'' lorsqu’une louange ou une adulation agit directement sur le comportement ou l’enthousiasme d’une personne.
=== Intérêt linguistique ===
Le mot ''audulard'' présente un intérêt particulier pour la langue française car il '''condense en un seul terme une nuance psychologique précise'''. Sa construction, issue du latin ''adulabilis'' (dérivé de ''adulor'', « flatter »), permet de rendre accessible à l’expression littéraire ou analytique la notion de réceptivité aux louanges.
En enrichissant le vocabulaire français, ''audulard'' offre ainsi une alternative élégante aux termes génériques comme ''flatteur'', ''complaisant'' ou ''sensible aux compliments'', tout en apportant une dimension stylistique et nuancée.
=== Étymologie ===
Du latin ''adulabilis'', dérivé de ''adulor'' (« flatter »), francisé pour devenir un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/5 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 15 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
69gxixb199i7s9vd16296d0ksbmdv7h
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/auricolore
2
83755
763135
763021
2026-04-07T15:57:05Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/auricolore]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/auricolore]] sans laisser de redirection : Contenu inédit : protologismes
763021
wikitext
text/x-wiki
== auricolore ==
''Adjectif''
'''Définition :'''
Se dit de ce qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante. Par extension, ''auricolore'' qualifie tout objet, matériau ou élément présentant une '''nuance chaude et lumineuse rappelant l’or'''<ref>https://limiel.omeka.net/items/show/77</ref>.
''Exemple :'' Ce bijou auricolore capte la lumière de manière étincelante.
=== Champ sémantique et usage ===
Le terme ''auricolore'' s’inscrit dans le champ des '''qualifications chromatiques et esthétiques'''. Il est particulièrement utile pour décrire avec précision les objets précieux, les bijoux, les tissus ou les éléments décoratifs qui possèdent une teinte dorée, là où le français courant recourt souvent à des expressions plus générales comme ''de couleur dorée'' ou ''doré''.
Une teinte ou un objet est dit ''auricolore'' lorsqu’il évoque la richesse, la chaleur et la luminosité de l’or.
=== Intérêt linguistique ===
''Auricolore'' présente un intérêt pour la langue française car il '''permet de condenser en un seul mot une nuance esthétique précise''', enrichissant le vocabulaire descriptif et poétique. Sa formation à partir du latin ''auricolor'' souligne à la fois la valeur stylistique et la précision lexicale de l'adjectif.
Ce mot est particulièrement adapté à la littérature, à la description artistique ou à tout contexte où '''la couleur et la brillance doivent être exprimées avec élégance et exactitude'''.
=== Étymologie ===
Du latin ''auricolor'' (« de couleur d’or »), francisé pour former un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/77 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
fq0ki8kex6vugmjmk6r0chrgxl6gnqv
763155
763135
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/auricolore]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/auricolore]] sans laisser de redirection
763021
wikitext
text/x-wiki
== auricolore ==
''Adjectif''
'''Définition :'''
Se dit de ce qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante. Par extension, ''auricolore'' qualifie tout objet, matériau ou élément présentant une '''nuance chaude et lumineuse rappelant l’or'''<ref>https://limiel.omeka.net/items/show/77</ref>.
''Exemple :'' Ce bijou auricolore capte la lumière de manière étincelante.
=== Champ sémantique et usage ===
Le terme ''auricolore'' s’inscrit dans le champ des '''qualifications chromatiques et esthétiques'''. Il est particulièrement utile pour décrire avec précision les objets précieux, les bijoux, les tissus ou les éléments décoratifs qui possèdent une teinte dorée, là où le français courant recourt souvent à des expressions plus générales comme ''de couleur dorée'' ou ''doré''.
Une teinte ou un objet est dit ''auricolore'' lorsqu’il évoque la richesse, la chaleur et la luminosité de l’or.
=== Intérêt linguistique ===
''Auricolore'' présente un intérêt pour la langue française car il '''permet de condenser en un seul mot une nuance esthétique précise''', enrichissant le vocabulaire descriptif et poétique. Sa formation à partir du latin ''auricolor'' souligne à la fois la valeur stylistique et la précision lexicale de l'adjectif.
Ce mot est particulièrement adapté à la littérature, à la description artistique ou à tout contexte où '''la couleur et la brillance doivent être exprimées avec élégance et exactitude'''.
=== Étymologie ===
Du latin ''auricolor'' (« de couleur d’or »), francisé pour former un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/77 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
fq0ki8kex6vugmjmk6r0chrgxl6gnqv
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/blatérate
2
83756
763137
763022
2026-04-07T15:57:05Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/blatérate]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/blatérate]] sans laisser de redirection : Contenu inédit : protologismes
763022
wikitext
text/x-wiki
== blatérate ==
''Adjectif'' <small>(voir aussi [[Guide des mots rares à adopter/blatération|''blatération'']])</small>
'''Définition :'''
Se dit de quelqu’un ou de quelque chose qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné. Par extension, ''blatérate'' qualifie une personne qui '''parle beaucoup pour ne rien dire''', multipliant les paroles creuses ou superficielles<ref>https://limiel.omeka.net/items/show/46</ref>.
''Exemple :'' La politicienne blatérate qu'était sa mère.
=== Champ sémantique et usage ===
Le terme ''blatérate'' s’inscrit dans le champ des '''paroles creuses, des discours superficiels ou des bavardages inutiles'''. Il permet de décrire avec précision un type de communication où le style peut tromper sur l’absence de contenu réel.
Un discours, un texte ou une personne est dite ''blatérate'' lorsqu’elle donne l’illusion de substance tout en restant vide de sens véritable.
=== Intérêt linguistique ===
''Blatérate'' enrichit la langue française en offrant '''un mot unique pour exprimer la vacuité du discours'''. Là où le français courant recourt à des périphrases comme ''parler pour ne rien dire'' ou ''discours creux'', ce néologisme permet de '''saisir à la fois la forme et le fond''' : le style trompeur et le manque de contenu.
Issu du latin ''blateratio'', il combine élégance et précision, et trouve sa place dans les écrits littéraires, critiques ou humoristiques pour qualifier avec justesse la prolixité vaine.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un adjectif expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/46 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
s2k8rdwbp1ag3kwj00jkjhiw7a8wru1
763157
763137
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/blatérate]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/blatérate]] sans laisser de redirection
763022
wikitext
text/x-wiki
== blatérate ==
''Adjectif'' <small>(voir aussi [[Guide des mots rares à adopter/blatération|''blatération'']])</small>
'''Définition :'''
Se dit de quelqu’un ou de quelque chose qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné. Par extension, ''blatérate'' qualifie une personne qui '''parle beaucoup pour ne rien dire''', multipliant les paroles creuses ou superficielles<ref>https://limiel.omeka.net/items/show/46</ref>.
''Exemple :'' La politicienne blatérate qu'était sa mère.
=== Champ sémantique et usage ===
Le terme ''blatérate'' s’inscrit dans le champ des '''paroles creuses, des discours superficiels ou des bavardages inutiles'''. Il permet de décrire avec précision un type de communication où le style peut tromper sur l’absence de contenu réel.
Un discours, un texte ou une personne est dite ''blatérate'' lorsqu’elle donne l’illusion de substance tout en restant vide de sens véritable.
=== Intérêt linguistique ===
''Blatérate'' enrichit la langue française en offrant '''un mot unique pour exprimer la vacuité du discours'''. Là où le français courant recourt à des périphrases comme ''parler pour ne rien dire'' ou ''discours creux'', ce néologisme permet de '''saisir à la fois la forme et le fond''' : le style trompeur et le manque de contenu.
Issu du latin ''blateratio'', il combine élégance et précision, et trouve sa place dans les écrits littéraires, critiques ou humoristiques pour qualifier avec justesse la prolixité vaine.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un adjectif expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/46 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
s2k8rdwbp1ag3kwj00jkjhiw7a8wru1
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/blatération
2
83757
763138
763023
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/blatération]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/blatération]] sans laisser de redirection : Contenu inédit : protologismes
763023
wikitext
text/x-wiki
== blatération ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/blatérate|blatérate]]'')</small>
'''Définition :'''
(''Ton satirique'') Désigne un '''bavardage frivole, superficiel''', une suite de paroles vaines ou légères, souvent répétitives et sans réel contenu<ref>https://limiel.omeka.net/items/show/45</ref>.
''Exemple :'' La blatération quotidienne des bourgeois rythmait les après‑midi du salon.
=== Champ sémantique et usage ===
Le terme ''blatération'' s’inscrit dans le champ des '''discours vides ou des bavardages futiles'''. Il est particulièrement adapté pour qualifier avec humour ou ironie des conversations où la forme prime sur le fond.
Une discussion, un récit ou une réunion peut être qualifiée de ''blatération'' lorsqu’elle se limite à des échanges superficiels, creux ou frivoles.
=== Intérêt linguistique ===
''Blatération'' enrichit la langue française en offrant '''un terme unique pour désigner le bavardage superficiel''', là où le français courant nécessite des périphrases comme ''paroles futiles'' ou ''bavardage vain''.
Issu du latin ''blateratio'', ce nom permet de conserver la nuance satirique et stylistique du mot, offrant une '''expression concise et élégante pour la critique sociale ou littéraire'''. Il est particulièrement utile en littérature, en critique sociale ou dans le registre humoristique pour décrire les excès de la parole vide.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un nom expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/45 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rg2ykr9k3mnsmhl483sxvgw1dbjov66
763158
763138
2026-04-07T15:57:25Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/blatération]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/blatération]] sans laisser de redirection
763023
wikitext
text/x-wiki
== blatération ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/blatérate|blatérate]]'')</small>
'''Définition :'''
(''Ton satirique'') Désigne un '''bavardage frivole, superficiel''', une suite de paroles vaines ou légères, souvent répétitives et sans réel contenu<ref>https://limiel.omeka.net/items/show/45</ref>.
''Exemple :'' La blatération quotidienne des bourgeois rythmait les après‑midi du salon.
=== Champ sémantique et usage ===
Le terme ''blatération'' s’inscrit dans le champ des '''discours vides ou des bavardages futiles'''. Il est particulièrement adapté pour qualifier avec humour ou ironie des conversations où la forme prime sur le fond.
Une discussion, un récit ou une réunion peut être qualifiée de ''blatération'' lorsqu’elle se limite à des échanges superficiels, creux ou frivoles.
=== Intérêt linguistique ===
''Blatération'' enrichit la langue française en offrant '''un terme unique pour désigner le bavardage superficiel''', là où le français courant nécessite des périphrases comme ''paroles futiles'' ou ''bavardage vain''.
Issu du latin ''blateratio'', ce nom permet de conserver la nuance satirique et stylistique du mot, offrant une '''expression concise et élégante pour la critique sociale ou littéraire'''. Il est particulièrement utile en littérature, en critique sociale ou dans le registre humoristique pour décrire les excès de la parole vide.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un nom expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/45 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rg2ykr9k3mnsmhl483sxvgw1dbjov66
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/burgenatif, burgenative
2
83758
763140
763024
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/burgenatif, burgenative]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/burgenatif, burgenative]] sans laisser de redirection : Contenu inédit : protologismes
763024
wikitext
text/x-wiki
== burgenatif, burgenative ==
''Nom et adjectif''
'''Prononciation :''' ''bur-gen-natif'' ou ''burgé-natif''
'''Définition (nom) :'''
# Personne née d'une '''famille riche ou bourgeoise''' ; par extension, quelqu’un qui '''vit dans une forteresse ou un château'''.
# Personne dont la '''réputation repose entièrement sur les actes ou la richesse de ses ancêtres'''. ''Exemple :'' Ces burgenatifs de la haute cité sont hautains envers les paysans.
'''Définition (adjectif) :'''
# Qualifie quelqu’un qui est né d’une famille riche ou bourgeoise ; par extension, vivant dans une forteresse ou un château.
# Qui tire sa réputation uniquement des actes ou de la richesse de ses ancêtres. ''Exemple :'' Les sieurs burgenatifs regardaient les nouveaux venus avec dédain<ref>https://limiel.omeka.net/items/show/51</ref>.
=== Champ sémantique et usage ===
Le terme ''burgenatif'' s’inscrit dans le champ des '''classes sociales, lignages et héritages de prestige'''. Il permet de décrire avec précision '''les personnes privilégiées par la naissance ou par l’héritage''', et peut être employé dans des contextes historiques, littéraires ou critiques.
Une personne est dite ''burgenative'' lorsqu’elle incarne '''l’aristocratie, le privilège familial ou le poids de la réputation ancestrale''', sans nécessairement avoir mérité son statut.
=== Intérêt linguistique ===
''Burgenatif'' est un mot rare d’intérêt pour la langue française, car il '''condense en un seul terme une idée complexe''' : la richesse héritée, le prestige familial et la réputation transmise par les ancêtres.
Sa formation originale résulte d’un '''télescopage lexical''' : le latin ''burgensis'' (habitant d’un bourg, bourgeois) combiné au français ''natif'', donnant un néologisme expressif, facilement mémorisable et utilisable à la fois comme nom et adjectif.
Ce mot enrichit le vocabulaire français en offrant '''une alternative concise et élégante''' pour évoquer les élites sociales et leur héritage, particulièrement utile en littérature, critique sociale ou écriture historique.
=== Étymologie ===
Du latin ''burgensis'' combiné morphologiquement au français ''natif'', francisé pour créer un terme descriptif et stylistique.
=== Source ===
[https://limiel.omeka.net/items/show/51 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
7fafx9zak0znsvk5bb04bakxemp8gc9
763160
763140
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/burgenatif, burgenative]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/burgenatif, burgenative]] sans laisser de redirection
763024
wikitext
text/x-wiki
== burgenatif, burgenative ==
''Nom et adjectif''
'''Prononciation :''' ''bur-gen-natif'' ou ''burgé-natif''
'''Définition (nom) :'''
# Personne née d'une '''famille riche ou bourgeoise''' ; par extension, quelqu’un qui '''vit dans une forteresse ou un château'''.
# Personne dont la '''réputation repose entièrement sur les actes ou la richesse de ses ancêtres'''. ''Exemple :'' Ces burgenatifs de la haute cité sont hautains envers les paysans.
'''Définition (adjectif) :'''
# Qualifie quelqu’un qui est né d’une famille riche ou bourgeoise ; par extension, vivant dans une forteresse ou un château.
# Qui tire sa réputation uniquement des actes ou de la richesse de ses ancêtres. ''Exemple :'' Les sieurs burgenatifs regardaient les nouveaux venus avec dédain<ref>https://limiel.omeka.net/items/show/51</ref>.
=== Champ sémantique et usage ===
Le terme ''burgenatif'' s’inscrit dans le champ des '''classes sociales, lignages et héritages de prestige'''. Il permet de décrire avec précision '''les personnes privilégiées par la naissance ou par l’héritage''', et peut être employé dans des contextes historiques, littéraires ou critiques.
Une personne est dite ''burgenative'' lorsqu’elle incarne '''l’aristocratie, le privilège familial ou le poids de la réputation ancestrale''', sans nécessairement avoir mérité son statut.
=== Intérêt linguistique ===
''Burgenatif'' est un mot rare d’intérêt pour la langue française, car il '''condense en un seul terme une idée complexe''' : la richesse héritée, le prestige familial et la réputation transmise par les ancêtres.
Sa formation originale résulte d’un '''télescopage lexical''' : le latin ''burgensis'' (habitant d’un bourg, bourgeois) combiné au français ''natif'', donnant un néologisme expressif, facilement mémorisable et utilisable à la fois comme nom et adjectif.
Ce mot enrichit le vocabulaire français en offrant '''une alternative concise et élégante''' pour évoquer les élites sociales et leur héritage, particulièrement utile en littérature, critique sociale ou écriture historique.
=== Étymologie ===
Du latin ''burgensis'' combiné morphologiquement au français ''natif'', francisé pour créer un terme descriptif et stylistique.
=== Source ===
[https://limiel.omeka.net/items/show/51 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
7fafx9zak0znsvk5bb04bakxemp8gc9
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/caullée
2
83759
763141
763017
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/caullée]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/caullée]] sans laisser de redirection : Contenu inédit : protologismes
763017
wikitext
text/x-wiki
== caullée ==
''Nom féminin''
'''Définition :'''
# Ensemble de '''[[wikt:cavité|cavités]] ou de [[wikt:précipice|précipices]] [[wikt:contigu|contigus]]'''. ''Exemple :'' La caullée du Grand Canyon s’étendait à perte de vue.
# '''[[wikt:enceinte|Enceinte]] ou [[wikt:clôture|clôture]] d’un chef-d’œuvre architectural ancien'''. ''Exemple :'' La caullée du temple de la vallée protégeait les fresques sacrées<ref>https://limiel.omeka.net/items/show/107</ref>.
=== Champ sémantique et usage ===
Le terme ''caullée'' s’inscrit dans le champ des '''formations géographiques et structures architecturales'''. Il permet de désigner avec précision '''un ensemble de cavités naturelles''' ou '''une enceinte architecturale complexe''', évitant les périphrases longues comme ''ensemble de trous ou de précipices'' ou ''enceinte ancienne''.
Une formation géologique ou un ouvrage ancien peut être qualifié de ''caullée'' lorsqu’il présente un regroupement cohérent et contigu de cavités, de creux ou de murs protecteurs.
=== Intérêt linguistique ===
''Caullée'' est un mot rare qui enrichit la langue française en offrant '''un terme précis et élégant pour décrire des structures naturelles ou architecturales'''. Sa formation, dérivée du latin ''caullæ'', combine simplicité et expressivité, permettant aux auteurs, historiens ou géographes de '''décrire des paysages ou des architectures avec concision et style'''.
Ce nom commun est particulièrement utile dans les textes littéraires ou scientifiques où la précision et la nuance sont essentielles.
=== Étymologie ===
Du latin ''caullæ'', francisé pour devenir un nom descriptif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/107 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 1er janvier 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
6980s2vqx6pspdrv9fpo00mzzchmuxi
763161
763141
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/caullée]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/caullée]] sans laisser de redirection
763017
wikitext
text/x-wiki
== caullée ==
''Nom féminin''
'''Définition :'''
# Ensemble de '''[[wikt:cavité|cavités]] ou de [[wikt:précipice|précipices]] [[wikt:contigu|contigus]]'''. ''Exemple :'' La caullée du Grand Canyon s’étendait à perte de vue.
# '''[[wikt:enceinte|Enceinte]] ou [[wikt:clôture|clôture]] d’un chef-d’œuvre architectural ancien'''. ''Exemple :'' La caullée du temple de la vallée protégeait les fresques sacrées<ref>https://limiel.omeka.net/items/show/107</ref>.
=== Champ sémantique et usage ===
Le terme ''caullée'' s’inscrit dans le champ des '''formations géographiques et structures architecturales'''. Il permet de désigner avec précision '''un ensemble de cavités naturelles''' ou '''une enceinte architecturale complexe''', évitant les périphrases longues comme ''ensemble de trous ou de précipices'' ou ''enceinte ancienne''.
Une formation géologique ou un ouvrage ancien peut être qualifié de ''caullée'' lorsqu’il présente un regroupement cohérent et contigu de cavités, de creux ou de murs protecteurs.
=== Intérêt linguistique ===
''Caullée'' est un mot rare qui enrichit la langue française en offrant '''un terme précis et élégant pour décrire des structures naturelles ou architecturales'''. Sa formation, dérivée du latin ''caullæ'', combine simplicité et expressivité, permettant aux auteurs, historiens ou géographes de '''décrire des paysages ou des architectures avec concision et style'''.
Ce nom commun est particulièrement utile dans les textes littéraires ou scientifiques où la précision et la nuance sont essentielles.
=== Étymologie ===
Du latin ''caullæ'', francisé pour devenir un nom descriptif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/107 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 1er janvier 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
6980s2vqx6pspdrv9fpo00mzzchmuxi
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/emphygomphe
2
83779
763147
763025
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/emphygomphe]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/emphygomphe]] sans laisser de redirection : Contenu inédit : protologismes
763025
wikitext
text/x-wiki
== emphygomphe ==
''Nom féminin''
'''Définition :'''
'''Proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux. Par extension, ''emphygomphe'' désigne '''tout discours pompeux où l’effet oratoire l’emporte sur le contenu réel'''<ref>https://limiel.omeka.net/items/show/188</ref>.
''Exemple'' : Son discours d’intronisation ne fut qu’une emphygomphe tonitruante, où l’orgueil faisait plus de bruit que les idées.
=== '''Champ sémantique et usage''' ===
Le terme ''emphygomphe'' s’inscrit dans le champ du langage oratoire et de la critique stylistique. Il est particulièrement adapté pour qualifier des discours officiels, politiques ou cérémoniels qui abusent de grandiloquence et de formules spectaculaires, là où le français courant recourt à des expressions comme discours pompeux ou tirade emphatique.
Un discours peut être qualifié d’emphygomphe lorsqu’il '''privilégie l’apparat verbal, l’enflure rhétorique et l’autoglorification au détriment de la clarté,''' de la sincérité ou de la profondeur des idées.
=== '''Intérêt linguistique''' ===
''Emphygomphe'' enrichit la langue française en offrant un terme précis et expressif pour désigner une forme particulière d’excès oratoire. Sa sonorité dense et légèrement burlesque évoque elle-même l’enflure qu’elle décrit, ce qui en fait '''un mot particulièrement efficace dans les contextes critiques ou satiriques.'''
Dans une perspective littéraire ou stylistique, ce terme permet de nommer avec concision un défaut fréquent du discours, tout en apportant une nuance ironique héritée de la tradition rabelaisienne.
=== '''Étymologie''' ===
Formé par télescopage lexical après francisation, à partir du latin ''gomphus'' et du grec ''γόμφος'', combinés à l’affixe latin ''emphy-'', pour produire un terme à forte expressivité phonétique et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/188 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 9 février 2026]
<small>(Lexème conçu pour la collection lexicale thématique « [https://limiel.omeka.net/collections/show/6 Dix néologismes conçus en hommage à François Rabelais] »)</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rt4ui6pgtevbh0wumocmubpdb4nuv60
763167
763147
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/emphygomphe]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/emphygomphe]] sans laisser de redirection
763025
wikitext
text/x-wiki
== emphygomphe ==
''Nom féminin''
'''Définition :'''
'''Proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux. Par extension, ''emphygomphe'' désigne '''tout discours pompeux où l’effet oratoire l’emporte sur le contenu réel'''<ref>https://limiel.omeka.net/items/show/188</ref>.
''Exemple'' : Son discours d’intronisation ne fut qu’une emphygomphe tonitruante, où l’orgueil faisait plus de bruit que les idées.
=== '''Champ sémantique et usage''' ===
Le terme ''emphygomphe'' s’inscrit dans le champ du langage oratoire et de la critique stylistique. Il est particulièrement adapté pour qualifier des discours officiels, politiques ou cérémoniels qui abusent de grandiloquence et de formules spectaculaires, là où le français courant recourt à des expressions comme discours pompeux ou tirade emphatique.
Un discours peut être qualifié d’emphygomphe lorsqu’il '''privilégie l’apparat verbal, l’enflure rhétorique et l’autoglorification au détriment de la clarté,''' de la sincérité ou de la profondeur des idées.
=== '''Intérêt linguistique''' ===
''Emphygomphe'' enrichit la langue française en offrant un terme précis et expressif pour désigner une forme particulière d’excès oratoire. Sa sonorité dense et légèrement burlesque évoque elle-même l’enflure qu’elle décrit, ce qui en fait '''un mot particulièrement efficace dans les contextes critiques ou satiriques.'''
Dans une perspective littéraire ou stylistique, ce terme permet de nommer avec concision un défaut fréquent du discours, tout en apportant une nuance ironique héritée de la tradition rabelaisienne.
=== '''Étymologie''' ===
Formé par télescopage lexical après francisation, à partir du latin ''gomphus'' et du grec ''γόμφος'', combinés à l’affixe latin ''emphy-'', pour produire un terme à forte expressivité phonétique et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/188 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 9 février 2026]
<small>(Lexème conçu pour la collection lexicale thématique « [https://limiel.omeka.net/collections/show/6 Dix néologismes conçus en hommage à François Rabelais] »)</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rt4ui6pgtevbh0wumocmubpdb4nuv60
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/brèviloquent, brèviloquente
2
83780
763139
763028
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/brèviloquent, brèviloquente]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/brèviloquent, brèviloquente]] sans laisser de redirection : Contenu inédit : protologismes
763028
wikitext
text/x-wiki
== brèviloquent, brèviloquente ==
''Adjectif''
'''Définition :'''
Se dit de ce qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''. Par extension, ''brèviloquent'' qualifie un style, un discours ou une personne '''qui s’exprime avec économie de mots tout en conservant clarté et précision'''<ref>https://limiel.omeka.net/items/show/202</ref>.
''Exemple'' : Son discours brèviloquent captivait l’auditoire par son économie de mots et sa précision incisive.
=== '''Champ sémantique et usage''' ===
Le terme ''brèviloquent'' s’inscrit dans le champ de l’expression verbale et du style rédactionnel. Il est particulièrement '''utile pour décrire des formes de communication efficaces, épurées et rigoureuses,''' là où le français courant recourt à des expressions comme concis, bref ou peu loquace.
Un discours ou un style est dit brèviloquent lorsqu’il parvient à transmettre l’essentiel avec justesse, sans digression inutile ni surcharge verbale.
=== '''Intérêt linguistique''' ===
''Brèviloquent'' enrichit la langue française en proposant un adjectif précis pour désigner la concision maîtrisée dans l’expression. Issu du latin ''breviloquens'' (« qui parle brièvement »), il '''permet de distinguer la simple brièveté d’une véritable qualité stylistique fondée sur la densité et la pertinence du propos'''.
Dans les domaines littéraire, académique ou professionnel, ce terme valorise une forme d’élégance discrète fondée sur la retenue et l’efficacité du langage.
=== '''Étymologie''' ===
Du latin ''breviloquens'' (« qui parle en peu de mots »), francisé pour former un adjectif descriptif et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/202 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
jrs5oy99t7m7p8yiq1l9q07zhgb7tcs
763159
763139
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/brèviloquent, brèviloquente]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/brèviloquent, brèviloquente]] sans laisser de redirection
763028
wikitext
text/x-wiki
== brèviloquent, brèviloquente ==
''Adjectif''
'''Définition :'''
Se dit de ce qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''. Par extension, ''brèviloquent'' qualifie un style, un discours ou une personne '''qui s’exprime avec économie de mots tout en conservant clarté et précision'''<ref>https://limiel.omeka.net/items/show/202</ref>.
''Exemple'' : Son discours brèviloquent captivait l’auditoire par son économie de mots et sa précision incisive.
=== '''Champ sémantique et usage''' ===
Le terme ''brèviloquent'' s’inscrit dans le champ de l’expression verbale et du style rédactionnel. Il est particulièrement '''utile pour décrire des formes de communication efficaces, épurées et rigoureuses,''' là où le français courant recourt à des expressions comme concis, bref ou peu loquace.
Un discours ou un style est dit brèviloquent lorsqu’il parvient à transmettre l’essentiel avec justesse, sans digression inutile ni surcharge verbale.
=== '''Intérêt linguistique''' ===
''Brèviloquent'' enrichit la langue française en proposant un adjectif précis pour désigner la concision maîtrisée dans l’expression. Issu du latin ''breviloquens'' (« qui parle brièvement »), il '''permet de distinguer la simple brièveté d’une véritable qualité stylistique fondée sur la densité et la pertinence du propos'''.
Dans les domaines littéraire, académique ou professionnel, ce terme valorise une forme d’élégance discrète fondée sur la retenue et l’efficacité du langage.
=== '''Étymologie''' ===
Du latin ''breviloquens'' (« qui parle en peu de mots »), francisé pour former un adjectif descriptif et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/202 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
jrs5oy99t7m7p8yiq1l9q07zhgb7tcs
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clausible
2
83781
763144
763026
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/clausible]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/clausible]] sans laisser de redirection : Contenu inédit : protologismes
763026
wikitext
text/x-wiki
== clausible ==
''Adjectif'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausibilité|clausibilité]]'')</small>
'''Définition :'''
'''Se dit de ce qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''', en conservant parfaitement son contenu ou sa structure. Par extension, ''clausible'' qualifie tout objet, système ou ensemble conçu pour être hermétiquement refermé de manière fiable<ref>https://limiel.omeka.net/items/show/290</ref>.
''Exemple'' : Ce contenant clausible se révèle particulièrement efficace pour préserver son contenu.
=== '''Champ sémantique et usage''' ===
Le terme ''clausible'' s’inscrit dans le champ des propriétés physiques et fonctionnelles des objets. Il est particulièrement '''utile pour décrire des contenants, dispositifs ou structures capables de se fermer de manière étanche et sécurisée''', là où le français courant recourt à des expressions comme refermable, hermétique ou à fermeture fiable.
Un objet est dit clausible lorsqu’il peut être fermé sans altération de son contenu, ni perte de substance, garantissant ainsi sa conservation ou son isolement.
=== '''Intérêt linguistique''' ===
''Clausible'' enrichit la langue française en proposant un adjectif précis pour désigner une propriété fonctionnelle spécifique, distincte de la simple possibilité de fermeture. Il introduit une nuance d’intégrité et de fiabilité dans l’acte de se refermer.
Dans les domaines technique, scientifique ou descriptif, '''ce terme permet d’exprimer avec concision une qualité''' souvent décrite par des périphrases plus longues, tout en conservant une élégance formelle.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), francisé pour former un adjectif descriptif et fonctionnel.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/290 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 28 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
kudr7612rs23yr7nbg3ql8e1vq4eusk
763164
763144
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/clausible]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clausible]] sans laisser de redirection
763026
wikitext
text/x-wiki
== clausible ==
''Adjectif'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausibilité|clausibilité]]'')</small>
'''Définition :'''
'''Se dit de ce qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''', en conservant parfaitement son contenu ou sa structure. Par extension, ''clausible'' qualifie tout objet, système ou ensemble conçu pour être hermétiquement refermé de manière fiable<ref>https://limiel.omeka.net/items/show/290</ref>.
''Exemple'' : Ce contenant clausible se révèle particulièrement efficace pour préserver son contenu.
=== '''Champ sémantique et usage''' ===
Le terme ''clausible'' s’inscrit dans le champ des propriétés physiques et fonctionnelles des objets. Il est particulièrement '''utile pour décrire des contenants, dispositifs ou structures capables de se fermer de manière étanche et sécurisée''', là où le français courant recourt à des expressions comme refermable, hermétique ou à fermeture fiable.
Un objet est dit clausible lorsqu’il peut être fermé sans altération de son contenu, ni perte de substance, garantissant ainsi sa conservation ou son isolement.
=== '''Intérêt linguistique''' ===
''Clausible'' enrichit la langue française en proposant un adjectif précis pour désigner une propriété fonctionnelle spécifique, distincte de la simple possibilité de fermeture. Il introduit une nuance d’intégrité et de fiabilité dans l’acte de se refermer.
Dans les domaines technique, scientifique ou descriptif, '''ce terme permet d’exprimer avec concision une qualité''' souvent décrite par des périphrases plus longues, tout en conservant une élégance formelle.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), francisé pour former un adjectif descriptif et fonctionnel.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/290 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 28 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
kudr7612rs23yr7nbg3ql8e1vq4eusk
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clausibilité
2
83782
763143
763027
2026-04-07T15:57:06Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/clausibilité]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/clausibilité]] sans laisser de redirection : Contenu inédit : protologismes
763027
wikitext
text/x-wiki
== clausibilité ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausible|clausible]]'')</small>
'''Définition :'''
Capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité'''. Par extension, ''clausibilité'' désigne la qualité intrinsèque de ce qui peut être fermé de manière fiable tout en préservant son contenu<ref>https://limiel.omeka.net/items/show/296</ref>.
''Exemple'' : La clausibilité du contenu lexicographique m’apparaît époustouflante.
=== '''Champ sémantique et usage''' ===
Le terme ''clausibilité'' s’inscrit dans le champ des propriétés structurelles et fonctionnelles. Il est particulièrement '''adapté pour désigner, de manière abstraite, l’aptitude d’un dispositif ou d’un ensemble à être refermé efficacement''', là où le français courant recourt à des expressions comme capacité de fermeture ou aptitude à être hermétique.
Une structure possède une clausibilité lorsqu’elle garantit une fermeture complète, sans altération ni fuite de ce qu’elle contient.
=== '''Intérêt linguistique''' ===
''Clausibilité'' enrichit le lexique en introduisant un nom abstrait dérivé directement de ''clausible'', permettant de conceptualiser une propriété technique ou théorique avec précision. Il offre ainsi un pendant nominal utile dans les discours scientifiques, techniques ou analytiques.
Ce terme '''permet de condenser en un seul mot une notion complexe''', évitant des formulations longues et favorisant une expression plus rigoureuse et structurée.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), à l’origine de l’adjectif ''[[Guide des mots rares à adopter/clausible|clausible]]'', dont dérive le nom ''clausibilité''.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/296 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 29 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
gh94oz4f3y7pq1ffevie9kwvkaoz9sh
763163
763143
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/clausibilité]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/clausibilité]] sans laisser de redirection
763027
wikitext
text/x-wiki
== clausibilité ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausible|clausible]]'')</small>
'''Définition :'''
Capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité'''. Par extension, ''clausibilité'' désigne la qualité intrinsèque de ce qui peut être fermé de manière fiable tout en préservant son contenu<ref>https://limiel.omeka.net/items/show/296</ref>.
''Exemple'' : La clausibilité du contenu lexicographique m’apparaît époustouflante.
=== '''Champ sémantique et usage''' ===
Le terme ''clausibilité'' s’inscrit dans le champ des propriétés structurelles et fonctionnelles. Il est particulièrement '''adapté pour désigner, de manière abstraite, l’aptitude d’un dispositif ou d’un ensemble à être refermé efficacement''', là où le français courant recourt à des expressions comme capacité de fermeture ou aptitude à être hermétique.
Une structure possède une clausibilité lorsqu’elle garantit une fermeture complète, sans altération ni fuite de ce qu’elle contient.
=== '''Intérêt linguistique''' ===
''Clausibilité'' enrichit le lexique en introduisant un nom abstrait dérivé directement de ''clausible'', permettant de conceptualiser une propriété technique ou théorique avec précision. Il offre ainsi un pendant nominal utile dans les discours scientifiques, techniques ou analytiques.
Ce terme '''permet de condenser en un seul mot une notion complexe''', évitant des formulations longues et favorisant une expression plus rigoureuse et structurée.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), à l’origine de l’adjectif ''[[Guide des mots rares à adopter/clausible|clausible]]'', dont dérive le nom ''clausibilité''.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/296 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 29 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
gh94oz4f3y7pq1ffevie9kwvkaoz9sh
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/culicellique
2
83784
763146
763033
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/culicellique]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/culicellique]] sans laisser de redirection : Contenu inédit : protologismes
763033
wikitext
text/x-wiki
== culicellique ==
''Adjectif''
'''Définition :'''
Se dit de ce qui rappelle par sa [[wikt:finesse|finesse]], sa [[wikt:légèreté|légèreté]] ou sa [[wikt:délicatesse|délicatesse]] '''l’aspect ou le [[wikt:comportement|comportement]] d’un tout petit insecte volant'''. Par extension, ''culicellique'' qualifie ce qui papillonne, frémit ou se meut avec agilité et subtilité<ref>https://limiel.omeka.net/items/show/201</ref>.
''Exemple'' : L’air culicellique de la clairière au matin frissonnait de chaque vibration légère, comme l’aile d’un petit insecte.
=== '''Champ sémantique et usage''' ===
Le terme ''culicellique'' s’inscrit dans le champ de la description sensorielle et poétique. Il est particulièrement '''utile pour évoquer des mouvements légers, des atmosphères aériennes ou des objets délicats''', là où le français courant recourt à des périphrases comme léger comme un insecte ou frémissant doucement.
Une personne, un objet ou un lieu peut être qualifié de culicellique lorsqu’il manifeste une grâce fragile, un mouvement subtil ou une légèreté aérienne.
=== '''Intérêt linguistique''' ===
''Culicellique'' enrichit le lexique français en offrant un adjectif '''précis pour exprimer légèreté, subtilité et finesse''', qualités difficiles à condenser en un seul mot. Sa formation, dérivée du latin ''culicellus'' (« petit moustique »), confère au terme une sonorité délicate et évocatrice, parfaitement adaptée à la poésie, à la littérature descriptive ou aux textes sensoriels.
Ce mot permet de transmettre en un seul terme la nuance fragile et aérienne d’un mouvement, d’une atmosphère ou d’un détail naturel.
=== '''Étymologie''' ===
Du latin ''culicellus'', diminutif de ''culex'' (« moustique »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/201 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
cokk5wr7v76x65xdcmhtfigddsdv6a3
763166
763146
2026-04-07T15:57:26Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/culicellique]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/culicellique]] sans laisser de redirection
763033
wikitext
text/x-wiki
== culicellique ==
''Adjectif''
'''Définition :'''
Se dit de ce qui rappelle par sa [[wikt:finesse|finesse]], sa [[wikt:légèreté|légèreté]] ou sa [[wikt:délicatesse|délicatesse]] '''l’aspect ou le [[wikt:comportement|comportement]] d’un tout petit insecte volant'''. Par extension, ''culicellique'' qualifie ce qui papillonne, frémit ou se meut avec agilité et subtilité<ref>https://limiel.omeka.net/items/show/201</ref>.
''Exemple'' : L’air culicellique de la clairière au matin frissonnait de chaque vibration légère, comme l’aile d’un petit insecte.
=== '''Champ sémantique et usage''' ===
Le terme ''culicellique'' s’inscrit dans le champ de la description sensorielle et poétique. Il est particulièrement '''utile pour évoquer des mouvements légers, des atmosphères aériennes ou des objets délicats''', là où le français courant recourt à des périphrases comme léger comme un insecte ou frémissant doucement.
Une personne, un objet ou un lieu peut être qualifié de culicellique lorsqu’il manifeste une grâce fragile, un mouvement subtil ou une légèreté aérienne.
=== '''Intérêt linguistique''' ===
''Culicellique'' enrichit le lexique français en offrant un adjectif '''précis pour exprimer légèreté, subtilité et finesse''', qualités difficiles à condenser en un seul mot. Sa formation, dérivée du latin ''culicellus'' (« petit moustique »), confère au terme une sonorité délicate et évocatrice, parfaitement adaptée à la poésie, à la littérature descriptive ou aux textes sensoriels.
Ce mot permet de transmettre en un seul terme la nuance fragile et aérienne d’un mouvement, d’une atmosphère ou d’un détail naturel.
=== '''Étymologie''' ===
Du latin ''culicellus'', diminutif de ''culex'' (« moustique »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/201 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
cokk5wr7v76x65xdcmhtfigddsdv6a3
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/imbrigène
2
83785
763150
763034
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/imbrigène]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/imbrigène]] sans laisser de redirection : Contenu inédit : protologismes
763034
wikitext
text/x-wiki
== imbrigène ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''[[wikt:naître|né]] de la [[wikt:pluie|pluie]] ou a été [[wikt:façonner|façonné]] par les [[wikt:goutte d’eau|gouttes d’eau]]'''. Par extension, ''imbrigène'' qualifie tout lieu, élément naturel ou phénomène directement influencé par la pluie ou l’humidité<ref>https://limiel.omeka.net/items/show/200</ref>.
''Exemple'' : Les marais imbrigènes couvrent une vaste partie de notre territoire ancestral.
=== '''Champ sémantique et usage''' ===
Le terme ''imbrigène'' s’inscrit dans le champ de la description géographique et écologique. Il est particulièrement '''utile pour évoquer des zones humides, des paysages pluvieux ou des éléments naturels créés par l’action de l’eau''', là où le français courant recourt à des périphrases comme né de la pluie ou façonné par les eaux.
Un terrain, un marais ou une végétation peut être qualifié d’imbrigène lorsqu’il témoigne directement de l’influence et de la présence persistante de la pluie.
=== '''Intérêt linguistique''' ===
''Imbrigène'' enrichit le lexique français en offrant un adjectif poétique et précis pour '''désigner des effets ou des origines liés à la pluie'''. Sa formation, dérivée du latin ''imbrigenus'' (« né de la pluie »), combine exactitude descriptive et dimension imagée, permettant d’évoquer avec concision des paysages humides et fertiles.
Ce mot est particulièrement adapté à la littérature naturaliste, à la poésie ou à toute description où l’eau et la pluie sont des acteurs essentiels du décor.
=== '''Étymologie''' ===
Du latin ''imbrigenus'' (« né de la pluie »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/200 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
[[Catégorie:Guide des mots rares à adopter]]
qmmcqcoa495d5t9hjqqptysmmav0h8p
763170
763150
2026-04-07T15:57:27Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/imbrigène]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/imbrigène]] sans laisser de redirection
763034
wikitext
text/x-wiki
== imbrigène ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''[[wikt:naître|né]] de la [[wikt:pluie|pluie]] ou a été [[wikt:façonner|façonné]] par les [[wikt:goutte d’eau|gouttes d’eau]]'''. Par extension, ''imbrigène'' qualifie tout lieu, élément naturel ou phénomène directement influencé par la pluie ou l’humidité<ref>https://limiel.omeka.net/items/show/200</ref>.
''Exemple'' : Les marais imbrigènes couvrent une vaste partie de notre territoire ancestral.
=== '''Champ sémantique et usage''' ===
Le terme ''imbrigène'' s’inscrit dans le champ de la description géographique et écologique. Il est particulièrement '''utile pour évoquer des zones humides, des paysages pluvieux ou des éléments naturels créés par l’action de l’eau''', là où le français courant recourt à des périphrases comme né de la pluie ou façonné par les eaux.
Un terrain, un marais ou une végétation peut être qualifié d’imbrigène lorsqu’il témoigne directement de l’influence et de la présence persistante de la pluie.
=== '''Intérêt linguistique''' ===
''Imbrigène'' enrichit le lexique français en offrant un adjectif poétique et précis pour '''désigner des effets ou des origines liés à la pluie'''. Sa formation, dérivée du latin ''imbrigenus'' (« né de la pluie »), combine exactitude descriptive et dimension imagée, permettant d’évoquer avec concision des paysages humides et fertiles.
Ce mot est particulièrement adapté à la littérature naturaliste, à la poésie ou à toute description où l’eau et la pluie sont des acteurs essentiels du décor.
=== '''Étymologie''' ===
Du latin ''imbrigenus'' (« né de la pluie »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/200 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
[[Catégorie:Guide des mots rares à adopter]]
qmmcqcoa495d5t9hjqqptysmmav0h8p
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/fâtiloque
2
83786
763149
763035
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/fâtiloque]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/fâtiloque]] sans laisser de redirection : Contenu inédit : protologismes
763035
wikitext
text/x-wiki
== fâtiloque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:prédire|prédit]] l’[[wikt:avenir|avenir]] ou [[wikt:connaître|connaît]] le [[wikt:destin|destin]]'''. Par extension, ''fâtiloque'' qualifie toute personne ou entité qui détient une voix sacrée, capable d’annoncer ou de révéler ce qui est à venir<ref>https://limiel.omeka.net/items/show/199</ref>.
''Exemple'' : J’ai toujours su que cette dame était une voyante fâtiloque.
=== '''Champ sémantique et usage''' ===
Le terme ''fâtiloque'' s’inscrit dans le champ de la divination, du présage et du mysticisme. Il est particulièrement adapté pour '''désigner des individus, des oracles ou des écrits réputés pour leur capacité à révéler l’avenir''', là où le français courant recourt à des expressions comme voyant, prophétique ou devin.
Une personne ou un discours peut être qualifié de fâtiloque lorsqu’il inspire confiance par sa sagesse, son autorité ou son pouvoir de révélation sur le destin.
=== '''Intérêt linguistique''' ===
''Fâtiloque'' enrichit le vocabulaire français en offrant un adjectif précis et poétique pour '''désigner la capacité de prédire ou de connaître le futur'''. Sa formation, dérivée du latin ''fatiloquus'' (« qui parle du destin »), confère au mot un caractère littéraire et solennel, idéal pour les contextes narratifs, poétiques ou historiques.
Ce mot permet d’exprimer en un seul terme la notion de voix prophétique ou sacrée, évitant des périphrases plus lourdes et renforçant l’effet stylistique du texte.
=== '''Étymologie''' ===
Du latin ''fatiloquus'' (« qui parle du destin »), francisé pour former un adjectif descriptif et littéraire.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/199 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
8vcnrhm9nsn6th8nn8c9r2ijexywt07
763169
763149
2026-04-07T15:57:27Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/fâtiloque]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/fâtiloque]] sans laisser de redirection
763035
wikitext
text/x-wiki
== fâtiloque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:prédire|prédit]] l’[[wikt:avenir|avenir]] ou [[wikt:connaître|connaît]] le [[wikt:destin|destin]]'''. Par extension, ''fâtiloque'' qualifie toute personne ou entité qui détient une voix sacrée, capable d’annoncer ou de révéler ce qui est à venir<ref>https://limiel.omeka.net/items/show/199</ref>.
''Exemple'' : J’ai toujours su que cette dame était une voyante fâtiloque.
=== '''Champ sémantique et usage''' ===
Le terme ''fâtiloque'' s’inscrit dans le champ de la divination, du présage et du mysticisme. Il est particulièrement adapté pour '''désigner des individus, des oracles ou des écrits réputés pour leur capacité à révéler l’avenir''', là où le français courant recourt à des expressions comme voyant, prophétique ou devin.
Une personne ou un discours peut être qualifié de fâtiloque lorsqu’il inspire confiance par sa sagesse, son autorité ou son pouvoir de révélation sur le destin.
=== '''Intérêt linguistique''' ===
''Fâtiloque'' enrichit le vocabulaire français en offrant un adjectif précis et poétique pour '''désigner la capacité de prédire ou de connaître le futur'''. Sa formation, dérivée du latin ''fatiloquus'' (« qui parle du destin »), confère au mot un caractère littéraire et solennel, idéal pour les contextes narratifs, poétiques ou historiques.
Ce mot permet d’exprimer en un seul terme la notion de voix prophétique ou sacrée, évitant des périphrases plus lourdes et renforçant l’effet stylistique du texte.
=== '''Étymologie''' ===
Du latin ''fatiloquus'' (« qui parle du destin »), francisé pour former un adjectif descriptif et littéraire.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/199 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
8vcnrhm9nsn6th8nn8c9r2ijexywt07
Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/fatilégue
2
83787
763148
763036
2026-04-07T15:57:07Z
JackPotte
5426
JackPotte a déplacé la page [[Guide des mots rares à adopter/fatilégue]] vers [[ROSEMARSH HOOD/Guide des mots rares à adopter/fatilégue]] sans laisser de redirection : Contenu inédit : protologismes
763036
wikitext
text/x-wiki
== fatilégue ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:récolter|récolte]] les [[wikt:âme|âmes]] ou [[wikt:colectionner|collectionne]] les [[wikt:mort|morts]]'''. Par extension, ''fatilégue'' qualifie tout être, figure ou allégorie associé à la mort et à la transition des vivants vers l’au-delà<ref>https://limiel.omeka.net/items/show/198</ref>.
''Exemple'' : La grande faucheuse, allégorie fatilégue.
=== '''Champ sémantique et usage''' ===
Le terme ''fatilégue'' s’inscrit dans le champ des représentations funéraires et symboliques de la mort. Il est particulièrement adapté pour '''désigner des figures mythiques, des entités littéraires ou des concepts métaphysiques liés au passage de la vie à la mort''', là où le français courant utilise des expressions comme faucheuse, passeuse d’âmes ou collectionneuse de morts.
Une allégorie, un personnage ou une figure peut être qualifié de fatilégue lorsqu’il incarne la fonction de guide ou de moissonneur des âmes, avec gravité et solennité.
=== '''Intérêt linguistique''' ===
''Fatilégue'' enrichit la langue française en offrant un adjectif précis et évocateur pour '''désigner la mort personnifiée ou ses agents symboliques'''. Sa formation, dérivée du latin ''fatilegus'' (« qui récolte le destin ou les âmes »), confère au mot une sonorité dramatique et poétique, adaptée à la littérature, à la mythologie ou à l’écriture allégorique.
Ce terme permet de condenser en un seul mot une idée complexe et solennelle, renforçant ainsi l’impact stylistique et imaginaire du texte.
=== '''Étymologie''' ===
Du latin ''fatilegus'' (« qui récolte les âmes »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/198 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
b3i777det3esckq6pzc5awdpwf953z8
763168
763148
2026-04-07T15:57:27Z
JackPotte
5426
JackPotte a déplacé la page [[ROSEMARSH HOOD/Guide des mots rares à adopter/fatilégue]] vers [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter/fatilégue]] sans laisser de redirection
763036
wikitext
text/x-wiki
== fatilégue ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:récolter|récolte]] les [[wikt:âme|âmes]] ou [[wikt:colectionner|collectionne]] les [[wikt:mort|morts]]'''. Par extension, ''fatilégue'' qualifie tout être, figure ou allégorie associé à la mort et à la transition des vivants vers l’au-delà<ref>https://limiel.omeka.net/items/show/198</ref>.
''Exemple'' : La grande faucheuse, allégorie fatilégue.
=== '''Champ sémantique et usage''' ===
Le terme ''fatilégue'' s’inscrit dans le champ des représentations funéraires et symboliques de la mort. Il est particulièrement adapté pour '''désigner des figures mythiques, des entités littéraires ou des concepts métaphysiques liés au passage de la vie à la mort''', là où le français courant utilise des expressions comme faucheuse, passeuse d’âmes ou collectionneuse de morts.
Une allégorie, un personnage ou une figure peut être qualifié de fatilégue lorsqu’il incarne la fonction de guide ou de moissonneur des âmes, avec gravité et solennité.
=== '''Intérêt linguistique''' ===
''Fatilégue'' enrichit la langue française en offrant un adjectif précis et évocateur pour '''désigner la mort personnifiée ou ses agents symboliques'''. Sa formation, dérivée du latin ''fatilegus'' (« qui récolte le destin ou les âmes »), confère au mot une sonorité dramatique et poétique, adaptée à la littérature, à la mythologie ou à l’écriture allégorique.
Ce terme permet de condenser en un seul mot une idée complexe et solennelle, renforçant ainsi l’impact stylistique et imaginaire du texte.
=== '''Étymologie''' ===
Du latin ''fatilegus'' (« qui récolte les âmes »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/198 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
b3i777det3esckq6pzc5awdpwf953z8
Discussion utilisateur:ROSEMARSH HOOD
3
83789
763175
2026-04-07T16:07:41Z
JackPotte
5426
Page créée avec « == [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] == Bonjour, je viens de déplacer votre livre dans l'espace utilisateur pour que vous puissiez le récupérer avant suppression, car il ne répond pas aux critères d'admissibilité. En effet, il traite de protologismes et non d'un contenu pédagogique académique, et à l'instar de Wikipédia [[Wikilivres/Relire un wikilivre|aucun contenu inédit]] n'est autorisé ici. Bien cordialement. ~~~~ »
763175
wikitext
text/x-wiki
== [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] ==
Bonjour, je viens de déplacer votre livre dans l'espace utilisateur pour que vous puissiez le récupérer avant suppression, car il ne répond pas aux critères d'admissibilité.
En effet, il traite de protologismes et non d'un contenu pédagogique académique, et à l'instar de Wikipédia [[Wikilivres/Relire un wikilivre|aucun contenu inédit]] n'est autorisé ici.
Bien cordialement. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 7 avril 2026 à 18:07 (CEST)
anohies7plv6ih2xtgkb65oi3lvvhva
763245
763175
2026-04-07T23:34:18Z
ROSEMARSH HOOD
122846
/* Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter */ Réponse
763245
wikitext
text/x-wiki
== [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] ==
Bonjour, je viens de déplacer votre livre dans l'espace utilisateur pour que vous puissiez le récupérer avant suppression, car il ne répond pas aux critères d'admissibilité.
En effet, il traite de protologismes et non d'un contenu pédagogique académique, et à l'instar de Wikipédia [[Wikilivres/Relire un wikilivre|aucun contenu inédit]] n'est autorisé ici.
Bien cordialement. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 7 avril 2026 à 18:07 (CEST)
:Bonjour et merci.
:Il ne s'agit pas de travail inédit mais plutôt d'un guide (ou mini-dictionnaire) de mots rares. Il ne traite guère de «protologismes» mais de néologismes qui sont des mots rares, tirés du LiMiEL, un dictionnaire en ligne. Nous sommes tous bénévoles ici, y compris moi. Je comprend que votre travail au sein de Wikilivres est d'une grande importance, or, je trouve grandement dommage que vous ayez supprimé un livre que j'ai pris plusieurs heures à écrire et qui visait à promouvoir des curiosités lexicales aux lecteurs et écrivains intéressés. À mon égard, le Guide des mots rares à adopter était utile ici. Il répondait également à tous les critères.
:Je ne veux pas davantage vous déranger dans votre précieux travail, mais j'ai mis beaucoup de temps et d'énergie sur ce livre et j'aimerais de meilleurs explication pour justifier votre suppression. Ou sinon, une restauration.
:Bien cordialement, [[Utilisateur:ROSEMARSH HOOD|ROSEMARSH HOOD]] ([[Discussion utilisateur:ROSEMARSH HOOD|discussion]]) 8 avril 2026 à 01:34 (CEST)
rwoykoy5x8e3s4q5lmmvu347x0c2qx7
763246
763245
2026-04-07T23:35:22Z
ROSEMARSH HOOD
122846
correction orthographe
763246
wikitext
text/x-wiki
== [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] ==
Bonjour, je viens de déplacer votre livre dans l'espace utilisateur pour que vous puissiez le récupérer avant suppression, car il ne répond pas aux critères d'admissibilité.
En effet, il traite de protologismes et non d'un contenu pédagogique académique, et à l'instar de Wikipédia [[Wikilivres/Relire un wikilivre|aucun contenu inédit]] n'est autorisé ici.
Bien cordialement. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 7 avril 2026 à 18:07 (CEST)
:Bonjour et merci.
:Il ne s'agit pas de travail inédit mais plutôt d'un guide (ou mini-dictionnaire) de mots rares. Il ne traite guère de «protologismes» mais de néologismes qui sont des mots rares, tirés du LiMiEL, un dictionnaire en ligne. Nous sommes tous bénévoles ici, y compris moi. Je comprend que votre travail au sein de Wikilivres est d'une grande importance, or, je trouve grandement dommage que vous ayez supprimé un livre que j'ai pris plusieurs heures à écrire et qui visait à promouvoir des curiosités lexicales aux lecteurs et écrivains intéressés. À mon égard, le Guide des mots rares à adopter était utile ici. Il répondait également à tous les critères.
:Je ne veux pas davantage vous déranger dans votre précieux travail, mais j'ai mis beaucoup de temps et d'énergie sur ce livre et j'aimerais de meilleures explications pour justifier votre suppression. Ou sinon, une restauration.
:Bien cordialement, [[Utilisateur:ROSEMARSH HOOD|ROSEMARSH HOOD]] ([[Discussion utilisateur:ROSEMARSH HOOD|discussion]]) 8 avril 2026 à 01:34 (CEST)
hh7up8hxmyj9i23q9lc2kga33evokxp
763247
763246
2026-04-08T07:55:00Z
JackPotte
5426
763247
wikitext
text/x-wiki
== [[Utilisateur:ROSEMARSH HOOD/Guide des mots rares à adopter]] ==
Bonjour, je viens de déplacer votre livre dans l'espace utilisateur pour que vous puissiez le récupérer avant suppression, car il ne répond pas aux critères d'admissibilité.
En effet, il traite de protologismes et non d'un contenu pédagogique académique, et à l'instar de Wikipédia [[Wikilivres/Relire un wikilivre|aucun contenu inédit]] n'est autorisé ici.
Bien cordialement. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 7 avril 2026 à 18:07 (CEST)
:Bonjour et merci.
:Il ne s'agit pas de travail inédit mais plutôt d'un guide (ou mini-dictionnaire) de mots rares. Il ne traite guère de «protologismes» mais de néologismes qui sont des mots rares, tirés du LiMiEL, un dictionnaire en ligne. Nous sommes tous bénévoles ici, y compris moi. Je comprend que votre travail au sein de Wikilivres est d'une grande importance, or, je trouve grandement dommage que vous ayez supprimé un livre que j'ai pris plusieurs heures à écrire et qui visait à promouvoir des curiosités lexicales aux lecteurs et écrivains intéressés. À mon égard, le Guide des mots rares à adopter était utile ici. Il répondait également à tous les critères.
:Je ne veux pas davantage vous déranger dans votre précieux travail, mais j'ai mis beaucoup de temps et d'énergie sur ce livre et j'aimerais de meilleures explications pour justifier votre suppression. Ou sinon, une restauration.
:Bien cordialement, [[Utilisateur:ROSEMARSH HOOD|ROSEMARSH HOOD]] ([[Discussion utilisateur:ROSEMARSH HOOD|discussion]]) 8 avril 2026 à 01:34 (CEST)
::Bonjour, le livre n'est pas encore supprimé, mais les liens de son sommaire étant statiques, ils n'ont pas supporté le renommage. On peut retrouver toutes les pages sur https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Index?prefix=ROSEMARSH+HOOD%2F&namespace=2.
::Concernant le débat "protologismes" versus "mots rares à adopter issu d'un dictionnaire", voici une autre explication qui j'espère, pourra mieux vous éclairer sur les critères de reconnaissance des œuvres : {{WP|Wikipédia:Travaux inédits}}. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 8 avril 2026 à 09:54 (CEST)
ia8a6p9884fdmy7i0g8k11nw25m5oaq