Les cartes graphiques/Le processeur de commandes
Pour rappel, les API 3D envoient des commandes graphiques standardisées au pilote de la carte graphique. Le pilote de périphérique transforme alors ces commandes graphiques, que le GPU ne peut pas comprendre, en commandes matérielles que le GPU connait. Et ce sont ces commandes matérielles qui sont exécutées par le GPU.
Dans les grandes lignes, on peut classer ces commandes matérielles en quelques types principaux : celles pour le rendu 3D, celles pour le rendu 2D, celles pour le décodage/encodage vidéo, celles pour le GPGPU, et les transferts DMA. Pour ce qui est du rendu 3D, les commandes matérielles ressemblent aux commandes graphiques de l'API, mais elles restent cependant différentes. Leur encodage est différent, elles ne font pas exactement la même chose, etc. Pour donner un exemple de commandes matérielles, prenons les commandes 2D de la carte graphique AMD Radeon X1800.
| Commandes 2D | Fonction |
|---|---|
| PAINT | Peindre un rectangle d'une certaine couleur |
| PAINT_MULTI | Peindre des rectangles (pas les mêmes paramètres que PAINT) |
| BITBLT | Copie d'un bloc de mémoire dans un autre |
| BITBLT_MULTI | Plusieurs copies de blocs de mémoire dans d'autres |
| TRANS_BITBLT | Copie de blocs de mémoire avec un masque |
| NEXTCHAR | Afficher un caractère avec une certaine couleur |
| HOSTDATA_BLT | Écrire une chaîne de caractères à l'écran ou copier une série d'image bitmap dans la mémoire vidéo |
| POLYLINE | Afficher des lignes reliées entre elles |
| POLYSCANLINES | Afficher des lignes |
| PLY_NEXTSCAN | Afficher plusieurs lignes simples |
| SET_SCISSORS | Utiliser des coupes (ciseaux) |
| LOAD_PALETTE | Charger la palette pour affichage 2D |
La gestion des commandes matérielle est le rôle du processeur de commande. Il s'occupe d'exécuter les commandes, en répartissant le travail sur les processeurs de shaders, en configurant les circuits fixes, en configurant les contrôleurs DMA, et bien d'autres choses.
L'architecture interne du processeur de commande
[modifier | modifier le wikicode]Le processeur de commande est un circuit assez compliqué. Sur les cartes graphiques anciennes, c'était un circuit séquentiel complexe, fabriqué à la main et était la partie la plus complexe du processeur graphique. Sur les cartes graphiques modernes, c'est un véritable microcontrôleur, avec un processeur, de la mémoire RAM, etc. Certains GPU utilisent parfois plusieurs microcontrôleurs séparés, avec souvent une séparation du processeur de commande en deux, voyons pourquoi.
Le processeur de commande a deux rôles : récupérer les commandes depuis la mémoire RAM, les exécuter. Pour rappel, les commandes matérielles sont accumulées dans une file de commande, en mémoire RAM ou en mémoire vidéo. Le processeur de commande lit la commande la plus ancienne dans la file de commande, puis l’exécute. Les deux fonctions sont souvent prises en charge par deux processeurs séparés. Le premier récupére les commandes dans la file de commandes et les envoie au second. Le second exécute les commandes, configure les circuits fixes du GPU, envoie les shaders sur les processeurs de shaders, et autres.
Il y a souvent une mémoire FIFO entre les deux, afin d'assouplir les transferts de données entre les deux processeurs de commande. L'intérêt est que le premier processeur de commande peut précharger à l'avance des commandes. Au lieu d'envoyer le commandes une par une au second processeur de commande, elle lui envoie à l'avance des commandes prêtes et les accumule dans la FIFO. Le second processeur de commande est alors constamment alimenté, les situations où il attend le premier processeur sont réduites.
Le second processeur de commande a plusieurs fonctionnalités : configurer les contrôleurs DMA intégrés au GPU, répartir les shaders sur les processeurs de shaders, gérer les commandes de changement d'état et de synchronisation (barrières et sémaphores), configurer les circuits fixes comme l’input assembler ou les ROP. Notamment, il répartit les shaders sur les processeurs de shaders, en faisant en sorte qu'ils soient utilisés le plus possible. Pour simplifier, il lance les shaders et gére le DMA.
Sur les anciens GPU, le processeur de commande cadencait le flot des données dans le pipeline graphique. Par exemple, si tous les processeurs de vertex shader sont occupés, l’input assembler ne peut pas charger le paquet suivant car il n'y a aucun processeur de libre pour l’accueillir. Dans ce cas, l'étape d’input assembly est mise en pause en attendant qu'un processeur de shader soit libre. La même chose a lieu pour l'étape de rastérisation : si aucun processeur de shader n'est libre, elle est mise en pause et les étages précédents sont potentiellement bloqués. Plus un étape est situé vers la fin du pipeline graphique, plus sa mise en pause a de chances de se répercuter sur les étapes précédentes et de les mettre en pause aussi. Le processeur de commande doit gérer ces situations.
La gestion du render state
[modifier | modifier le wikicode]S'il y a une différence entre les commandes matérielles et les commandes graphiques, il en est de même pour le render state. Le GPU a bien besoin de mémoriser des informations de configuration, quelles textures utiliser, quels shaders exécuter, etc. Mais autant l'API regroupe le tout dans un render state unique, autant ce n'est pas forcément le cas sur le GPU.
La manière la plus simple mémorise le render state tel quel, dans divers registres de configuration, dispersés dans le GPU, souvent dans les unités non-programmables. Les unités de texture mémorisent les textures à rendre, ainsi que les options de filtrage de texture. Les ROP mémorisent les options de résolution, d'antialiasing et celles pour le tampon de profondeur. Et ainsi de suite. Tout changement dans le render state correspondra à une commande matérielle qui modifiera le registre adéquat. Changer de shader ou de texture demande de modifier un registre qui pointe vers la texture ou le shader, cela revient à modifier un pointeur mémorisé dans un registre.
Mais les GPU modernes ne font pas cela à la lettre. Des informations ne sont pas mémorisées dans des registres statiques et sont transmises dans le pipeline, d'étage en étage. Par exemple, les unité de textures sont totalement bindless : le shader leur envoie l'adresse de la texture à utiliser à chaque lecture, elles n'ont pas besoin de la mémoriser dans un registre. En clair, changer de texture changera le render state, mais n'impliquera pas de commande de changement d'état.
L'exécution parallèle des commandes
[modifier | modifier le wikicode]Un processeur de commande basique exécute les commandes l'une après l'autre. Mais un processeur de commande évolué peut parfois exécuter plusieurs commandes en même temps. Nous verrons dans le chapitre suivant comment une telle sorcellerie est possible pour les commandes de rendu 3D. Mais n'oublions pas les autres commandes qui n'ont rien à voir avec le rendu 3D : commandes de rendu 2D, de calcul GPGPU, de décodage vidéo, ou des transferts DMA.
Il est possible d'exécuter des commandes de type différent, sous certaines conditions. Par exemple, exécuter un transfert DMA pendant que le GPU fait un rendu 2D ou des calculs GPGPU. Mais il faut pour cela que les deux commandes n'utilisent pas les mêmes circuits. Le rendu 2D et 3D accèdent tous deux au framebuffer, ce qui fait qu'on ne peut pas lancer une commande 2D et une commande 3D en même temps, du moins sans les optimisations qu'on verra dans le prochain chapitre. Il en est de même avec le GPGPU, le rendu 2D et le rendu 3D, qui utilisent tous les trois les processeurs de shaders.
Par contre, les processeurs de shader peuvent traiter une commande 3D, alors qu'une commande de transfert DMA est en cours dans le contrôleur DMA. Les deux sont traités dans des circuits séparés, si on omet le fait que les deux adressent la VRAM, mais les deux peuvent se partager la bande passante mémoire naturellement, sans intervention du processeur de commande. La seule contrainte est que le transfert DMA et la commande 3D n'utilisent pas les mêmes zones de mémoire. En clair, il ne faut pas faire de transfert DMA dans un tampon de sommet si celui-ci est en cours d'utilisation. Mais le pilote de périphérique peut détecter ce genre de cas et le processeur de commande peut bloquer l'exécution de la seconde commande si besoin.
La synchronisation avec le processeur
[modifier | modifier le wikicode]Il arrive que le processeur doive savoir où en est le traitement des commandes, ce qui est très utile pour la gestion de la mémoire vidéo. Par exemple, comment éviter d'enlever une texture tant que les commandes qui l'utilisent ne sont pas terminées ? Ce problème ne se limite pas aux textures, mais vaut pour tout ce qui est placé en mémoire vidéo. Il faut donc que la carte graphique trouve un moyen de prévenir le processeur que le traitement de telle commande est terminée, que telle commande en est rendue à tel ou tel point, etc.
Pour cela, on a globalement deux grandes solutions. La première est celle des interruptions, une fonctionnalité de communication entre CPU et périphérique, qui sont gérées par le pilote du GPU. Mais elles sont assez couteuses et ne sont utilisées que pour des évènements vraiment importants, critiques, qui demandent une réaction rapide du processeur. L'autre solution est celle du pooling, où le processeur monitore la carte graphique à intervalles réguliers. Deux solutions bien connues de ceux qui ont déjà lu un cours d'architecture des ordinateurs digne de ce nom, rien de bien neuf : la carte graphique communique avec le processeur comme tout périphérique.
Pour faciliter l'implémentation du pooling, la carte graphique contient des registres de statut, dans le processeur de commande, qui mémorisent tout ou partie de l'état de la carte graphique. Si un problème survient, certains bits du registre de statut seront mis à 1 pour indiquer que telle erreur bien précise a eu lieu. Il existe aussi un registre de statut qui mémorise le numéro de la commande en cours, ou de la dernière commande terminée, ce qui permet de savoir où en est la carte graphique dans l'exécution des commandes. Et certaines registres de statut sont dédiés à la gestion des commandes. Ils sont totalement programmable, la carte graphique peut écrire dedans sans problème.
Les cartes graphiques récentes incorporent des commandes pour modifier ces registres de statut au besoin. Elles sont appelées des commandes de synchronisation : les barrières (fences). Elles permettent d'écrire une valeur dans un registre de statut quand telle ou telle condition est remplie. Par exemple, si jamais une commande est terminée, on peut écrire la valeur 1 dans tel ou tel registre. La condition en question peut être assez complexe et se résumer à quelque chose du genre : "si jamais toutes les shaders ont fini de traiter tel ensemble de textures, alors écrit la valeur 1024 dans le registre de statut numéro 9".
L'exemple du processeur de commande du NV1
[modifier | modifier le wikicode]Pour donner un exemple, prenons la première carte graphique de NVIDIA, le NV1. Il s'agissait d'une carte multimédia qui incorporait non seulement une carte 2D/3D, mais aussi une carte son un contrôleur de disque et de quoi communiquer avec des manettes.
Il utilisait une mémoire FIFO dédiée aux commandes matérielles. Le processeur pouvait ainsi envoyer plusieurs commandes au GPU, en une seule transaction. Il y avait donc une copie de la file de commandes vers cette FIFO. Le GPU exécutait alors les commandes une par une. Le processeur pouvait consulter la FIFO pour savoir s'il restait des entrées libres et combien. Le processeur savait ainsi combien d'écritures il pouvait envoyer en une fois au GPU.
- Le fonctionnement de la FIFO du NV1 est décrit dans le brevet US5805930A : System for FIFO informing the availability of stages to store commands which include data and virtual address sent directly from application programs.
En plus de cette FIFO de commandes, il incorporait un contrôleur DMA pour échanger des données entre RAM système et les autres circuits. Lorsque le processeur voulait copier des données en mémoire vidéo, il envoyait une commande de copie, qui était stockée dans la FIFO de commande, puis était exécutée par le processeur de commande. Le processeur de commande envoyait alors les ordres adéquats au contrôleur DMA, qui faisait la copie des données.

Le NVIDIA NV1 avait diverses optimisations pour supporter plusieurs applications à la fois. L'une d'entre elle est le support de plusieurs tampons de commande. La carte graphique gérait en tout 128 tampons de commandes, chacun contenant 32 commandes consécutives. Le tout permettait à 128 logiciels différents d'avoir chacun son propre tampon de commande. L'implémentation du NV1 utilisait en réalité une FIFO unique, dans une mémoire RAM unique, qui était segmentée en 128 sous-FIFOs. Mais il est techniquement possible d'utiliser plusieurs FIFOs séparées connectées à un multiplexeur en sortie et un démultiplexeur en entrée.
Le NV1 utilisait des adresses de 23 bits, ce qui fait 8 méga-octets de RAM. Les 8 méga-octets étaient découpées en 128 blocs de mémoire consécutifs, chacun étant associé à une application et faisant 64 kilo-octets. Les adresses de 23 bits étaient donc découpées en une portion de 7 bit pour identifier le logiciel qui envoie la commande, et une portion de 16 bits pour identifier la position des données dans le bloc de RAM. Une entrée dans la FIFO du NV1 faisait 48 bits, contenant une donnée de 32 bits et les 16 bits de l'adresse.