Les cartes graphiques/Le rendu d'une scène 3D : l'API graphique
De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation.
La description des API 3D les plus communes
[modifier | modifier le wikicode]Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas.
Les draw calls
[modifier | modifier le wikicode]Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le framebuffer. Elle est appelée draw() dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un draw call. Un draw callenvoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé.
Instinctivement, on pourrait croire que la fonction draw calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un draw call par objet. Plus il y a d'objets, plus le processeur exécutera de draw calls. Diverses optimisations permettent d'économiser des draw calls, mais cela ne change pas le fait que dessiner l'image finale demande plusieurs draw calls, entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre.
Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. L'avantage est que l'élimination des surfaces cachées fonctionne mieux. Comme autre exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection: aucun triangle/pixel caché par un autre ne sera rendu. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le CPU.
Une autre optimisation est l'élimination des surface cachées, qui peut être prise en charge en partie par le CPU et en partie par le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de draw calls pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle.
De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un shader séparé, un compute shader, qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le compute shader lit l'ensemble de la géométrie et élimine les surface cachées. On parle de GPU driven rendering pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du Level Of Detail, mais passons.
Les render target
[modifier | modifier le wikicode]Plus haut, j'ai dit qu'un draw call dessine une image dans le framebuffer. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un pixel shader pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du render-to-texture.
Les techniques d'éclairage basées sur des shadowmap sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une shadowmap.
Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un shader lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le framebuffer s'il calcule l'image finale.
Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le framebuffer, dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des render target. Les API modernes supportent de nombreux render target, avec au minimum un framebuffer. Initialement, les API anciennes ne supportaient que le framebuffer. Puis le render-to-texture est apparu, puis d'autres formes de render target.
Il faut noter que les API modernes permettent à un shader d'écrire dans plusieurs render-target. On parle alors de Multiple Render Targets, abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale.
L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un draw call par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N draw call pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA.
Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le pixel shader, il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage.
Il faut alors supporter des pseudo-framebuffer pour chaque "texture", appelés des G-buffer, pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un draw call par G-buffer, chacun recalculant la géométrie. Avec le MRT, les différents G-buffer sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois.
Les render states et les Pipeline State Object
[modifier | modifier le wikicode]Pour rendre un objet avec un draw call, il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les shaders à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un mesh qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un render state, qui liste les textures, les shaders, quel render starget utiliser, et surtout : diverses options de configuration.
Il n'y a qu'un seul render state actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le render state est dans une variable globale, qui est lue directement par la fonction draw. Si on veut rendre un objet, on doit mettre à jour le render state avant de lancer un draw call. Un moteur graphique fait donc le travail suivant :
- Pour chaque image :
- Mettre à jour la position de la caméra et autres
- Pour chaque objet, scène 3D inclue :
- 1 - Mettre à jour le render state
- 2 - Exécuter le draw call
L'API 3D fournit des fonctions pour modifier le render state, en plus de la fonctiondraw. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing.
Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le render state assez simplement. L'idée est de pré-calculer un render state, qui est alors appelé un Pipeline State Object (PSO). Mettre à jour le render state demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le render state voulu.
Les commandes graphiques
[modifier | modifier le wikicode]L'API 3D traduit chaque draw call en une ou plusieurs commandes graphiques, qui sont envoyées au driver du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un draw call correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/mesh.
Pour comprendre en quoi un draw call peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le draw call utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le framebuffer. Par contre, si la texture est déjà en mémoire vidéo, le draw call se traduira en une unique commande de rendu 3D. Il en est de même si le mesh n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le mesh dans la mémoire vidéo.
Il faut préciser que c'est la même chose si le draw call exécute un shader pour la première fois . Le driver doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le shader stuttering présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un shader est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs draw calls, dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible.
Les commandes graphiques sont envoyées au driver de la carte graphique. Il transforme alors ces commandes graphiques en commandes matérielles, compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande.
Les optimisations liées aux draw calls
[modifier | modifier le wikicode]Il faut noter qu'un draw call demande d'utiliser un peu de puissance CPU : il faut traduire le draw call en commandes, les envoyer au driver, qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque draw call effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le driver. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de draw calls aura donc un cout en CPU conséquent.
Une première optimisation regroupe les objets avec le même render state ensemble. Sans cette optimisation, le moteur graphique met à jour le render state à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le render state plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de draw calls.
Plus haut, j'ai dit que le rendu se fait objet par objet, mesh par mesh. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits draw call : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros draw call entrainera une forte occupation du GPU au prix d'un cout CPU mineur.
La première optimisation, appelée le batching, regroupe plusieurs objets/meshs en un seul draw call. Par contre, cette optimisation ne marche que pour des objets ayant le même render state, à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image.
Diverses optimisations permettent de faciliter le batching. L'idée est de rendre les différents render state plus similaires que la normale. Une optimisation de ce type est l'usage d'atlas de textures. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même render state quand ils adressent le même atlas de texture et non exactement les mêmes textures.
Une seconde optimisation,appelée l'instancing, marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un draw call par exemplaire, on utilise un seul draw call pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même mesh.
Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le draw call précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le render state contient pour cela une liste d'instances pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le mesh et dessine automatiquement chaque exemplaire voulu de l'objet.
Réduire le nombre de draw calls peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le draw call très vite et devra attendre que le CPU envoie le suivant. Le cout du draw call dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative.
Le pipeline graphique
[modifier | modifier le wikicode]En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter.
Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète.
Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (per vertex operations et primitive assembly), la rastérisation, et les traitements sur les pixels (per fragment operations). On y voit la présence du framebuffer et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo.
La display list est une liste de commandes, de draw calls, que la carte graphique doit traiter d'un seul bloc, chaque display list correspond au rendu d'une image, pour simplifier. Les étapes evaluator et pixel operations sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques.

Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes).
L'implémentation peut être logicielle ou matérielle
[modifier | modifier le wikicode]Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D.
D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer.
De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les draw calls sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D.
Les API imposent des contraintes sur le matériel
[modifier | modifier le wikicode]Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API.
Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc.
Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du culling ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne.
De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D.
Le pilote de carte graphique
[modifier | modifier le wikicode]Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux.
Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux.
Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent.
Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le pooling ou le DMA. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le framebuffer, les render target et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des draw call et des changements de render state. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D.
Les commandes matérielles, compréhensibles par le GPU
[modifier | modifier le wikicode]Pour rappel, les API 3Denvoient des commandes graphiques au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des commandes matérielles, spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre.
- La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau.
L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une file de commandes, où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM.
Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment.
La compilation des shaders
[modifier | modifier le wikicode]Le pilote de carte graphique traduit les shaders en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les shaders sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres.
En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des shaders, alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les shaders à chaque exécution.
Fait amusant, il faut savoir que le pilote peut parfois remplacer les shaders d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des shaders alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le shader originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le shader alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire.
Enfin, certains shaders sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de shaders fournit par le pilote de carte graphique.





