Les cartes graphiques/Les cartes accélératrices 2D
Avec l'arrivée des interfaces graphiques (celles des systèmes d'exploitation, notamment) et des jeux vidéo 2D, les cartes graphiques ont pu s'adapter. Les cartes graphiques 2D ont d'abord commencé par accélérer le tracé et coloriage de figures géométriques simples, tels les lignes, segments, cercles et ellipses, afin d’accélérer les premières interfaces graphiques. Par la suite, diverses techniques d'accélération de rendu 2D se sont fait jour.
Ces techniques ne sont pas utiles que pour les jeux vidéo, mais peuvent aussi servir à accélérer le rendu d'une interface graphique. Après tout, les lettres, les fenêtres d'une application ou le curseur de souris sont techniquement du rendu 2D. C'est ainsi que les cartes graphiques actuelles supportent des techniques d’accélération du rendu des polices d'écriture, une accélération du défilement ou encore un support matériel du curseur de la souris, toutes dérivées des techniques d'accélération de rendu 2D.
La base d'un rendu en 2D est de superposer des images 2D pré-calculées les unes au-dessus des autres. Par exemple, on peut avoir une image pour l’arrière-plan (le décor), une image pour le monstre qui vous fonce dessus, une image pour le dessin de votre personnage, etc. On distingue généralement l'image pour l'arrière-plan, qui prend tout l'écran, des images plus petites qu'on superpose dessus et qui sont appelées des sprites. Le rendu des sprites doit s'effectuer de l'image la plus profonde (l'arrière-plan), vers les plus proches (les sprites qui se superposent sur les autres) : on parle d'algorithme du peintre.
Il existe plusieurs techniques d'accélération graphique pour le rendu en 2D :
- L'accélération des copies dans la mémoire vidéo grâce à un circuit dédié. Elle aide à implémenter de manière rapide le défilement ou les sprites, ou encore le tracé de certaines figures géométriques. Mais elle est moins performante que les trois suivantes, bien qu'elle lui soit complémentaire.
- L'accélération matérielle des sprites, où les sprites sont stockés dans des registres dédiés et où la carte graphique les gère séparément de l'arrière-plan.
- L'accélération matérielle du défilement, une opération très couteuse.
- L'accélération matérielle du tracé de lignes/segments/figures géométriques simples.
Les quatre techniques ne sont pas exclusives, mais complémentaires.
Le circuit de Blitter : les copies en mémoire
modifierLes cartes 2D ont introduit un circuit pour accélérer les copies d'images en mémoire, appelé le blitter. Les copies de données en mémoire vidéo sont nécessaires pour ajouter les sprites sur l'arrière-plan, mais aussi lorsqu'un objet 2D se déplace sur l'écran. Déplacer une fenêtre sur votre bureau est un bon exemple : le contenu de ce qui était présent sur l'écran doit être déplacé vers le haut ou vers le bas. Dans la mémoire vidéo, cela correspond à une copie des pixels correspondant de leur ancienne position vers la nouvelle.
Sans blitter, les copies étaient donc à la charge du processeur, qui devait déplacer lui-même les données en mémoire. Le blitter est conçu pour ce genre de tâches, sauf qu'il n'utilise pas le processeur.
- Pour ceux qui ont quelques connaissances avancées en architecture des ordinateurs, on peut le voir comme une sorte de contrôleur DMA amélioré, avec une gestion d'opérations annexes autres que la copie. D'ailleurs, il est souvent combiné à un contrôleur DMA, voire fusionné avec lui.
La superposition des sprites sur l'arrière-plan
modifierLes cartes 2D sans sprites matériels effectuent leur rendu en deux étapes : elles copient l'image d'arrière-plan dans le framebuffer, puis chaque sprite est copié à la bonne palce en mémoire pour le placer au bon endroit sur l'écran. Les copies de sprites sont généralement prises en charge par le blitter, qui est spécialement optimisé pour cela. L'optimisation principale est le fait que le blitter peut effectuer une opération bit à bit entre les données à copier et une donnée fournie par le programmeur appelé un masque.
Pour voir à quoi cela peut servir, rappelons que les sprites sont souvent des images rectangulaires sans transparence ! Le sans transparence est très important pour ce qui va suivre. Idéalement, les sprites devraient contenir des zones transparentes pour laisser la place à l'arrière-plan, mais le hardware ne gère pas forcément des pixels transparents à l'intérieur des sprites. Emuler la transparence peut s'émuler facilement en utilisant un masque, une donnée qui indique quelles parties de l'image sont censées être transparentes.
Par exemple, l'image correspondant à notre bon vieux pacman ressemblerait à celle ci-dessous, à gauche. Les parties noires de l'image du pacman sont censées être transparentes et ne pas être copiées au-dessus de l'arrière-plan, il ne faut le faire que pour les pixels jaunes. Le masque qui indique quels pixels sont à copier ou non, est celui situé à droite.
Le blitter prend l'image du pacman, le morceau de l’arrière-plan auquel on superpose pacman, et le masque. Pour chaque pixel, il effectue l'opération suivante : ((arrière-plan) AND (masque)) OR (image de pacman). L'application d'un ET logique entre masque et arrière-plan met à zéro les pixels modifiés par le sprite et uniquement ceux-ci, ils sont mis à la couleur noire. Le OU copie le sprite dans le vide laissé. La première étape est nécessaire, car un OU avec un arrière-plan non-noir donnerait un mélange bâtard entre arrière-plan et sprite. Au final, l'image finale est bel et bien celle qu'on attend.
Les sprites matériels
modifierIl existe des cartes 2D sur lesquelles les sprites sont gérés directement en matériel, sans passer par un blitter, sans être copiés sur un arrière-plan préexistant. À la place, l'image est rendue pixel par pixel, à la volée. La carte graphique décide, à chaque envoi de pixel, s'il faut afficher les pixels de l’arrière-plan ou du sprite pendant l'accès au framebuffer par le CRTC.
Les circuits d'accélération matérielle des sprites
modifierSur ces cartes 2D, les sprites sont stockés dans des registres, alors que l'image d'arrière-plan est stockée dans un framebuffer, dans une mémoire RAM. La RAM pour l'arrière-plan est généralement assez grosse, car l'arrière-plan a la même résolution que l'écran. Pour les sprites, la mémoire est généralement très petite, ce qui fait que les sprites ont une taille limitée.
Pour chaque sprite, on trouve deux registres permettant de mémoriser la position du sprite à l'écran : un pour sa coordonnée X, un autre pour sa coordonnée Y. Lorsque le CRTC demande à afficher le pixel à la position (X , Y), chaque triplet de registres de position est comparé à la position X,Y fournie par le CRTC. Si aucun sprite ne correspond, les mémoires des sprites sont déconnectées du bus et le pixel affiché est celui de l'arrière-plan. Dans le cas contraire, la RAM du sprite est connectée sur le bus et son contenu est envoyé au RAMDAC.
Les cartes 2D les plus simples ne géraient que deux niveaux : soit l'arrière-plan, soit un sprite devant l'arrière-plan. Il n'est donc pas possible que deux sprites se superposent, partiellement ou totalement. Dans ce cas, l'image n'a que deux niveaux de profondeur. C'était le cas sur les consoles de seconde et troisième génération, comme la NES ou la Sega Saturn. Par la suite, une gestion des sprite superposés est apparue. Pour cela, il fallait stocker la profondeur de chaque sprite, pour savoir celui qui est superposé au-dessus de tous les autres. Cela demandait d'ajouter un registre pour chaque sprite, qui mémorisait la profondeur. Le circuit devait donc être modifié de manière à gérer la profondeur, en gérant la priorité des sprites.
D'autres consoles ont ajouté une gestion de la transparence. Dans le cas le plus simple, la transparence permet de ne pas afficher certaines portions d'un sprite. Certains pixels d'un sprite sont marqués comme transparents, et à ces endroits, c'est l'arrière-plan qui doit s'afficher. Cela permet d'afficher des personnages ou objets complexes alors que l'image du sprite est rectangulaire. Le choix du pixel à afficher dépend alors non seulement de la profondeur, mais aussi de la transparence des pixels des sprite. Le pixel d'un sprite a la priorité sur le reste s'il est opaque et que sa profondeur est la plus faible comparée aux autres. Cette gestion basique de la transparence ne permet pas de gérer des effets trop complexe, où on mélange la couleur du sprite avec celle de l'arrière-plan.
Le support des sprites est parfois utilisé dans un cadre particulièrement spécialisé : la prise en charge du curseur de la souris, ou le rendu de certaines polices d'écritures ! Le curseur de souris est alors traité comme un sprite spécialisé, surimposé au-dessus de tout le reste.
Les cartes graphiques modernes gèrent un ou plusieurs sprites, qui représentent chacun un curseur de souris, et deux registres, qui stockent les coordonnées x et y du curseur. Ainsi, pas besoin de redessiner l'image à envoyer à l'écran à chaque fois que l'on bouge la souris : il suffit de modifier le contenu des deux registres, et la carte graphique place le curseur sur l'écran automatiquement. Pour en avoir la preuve, testez une nouvelle machine sur laquelle les drivers ne sont pas installés, et bougez le curseur : lag garantit !
Le stockage des sprites et de l’arrière-plan : les tiles
modifierLes sprites et l'arrière-plan sont donc stockés chacun dans une mémoire dédiée. En théorie, les sprites et l'arrière plan sont des images tout ce qu'il y a de plus normales, et elles devraient être codées telles quelles. Mais cela demanderait d'utiliser des mémoires assez grosses. Notamment, l'arrière-plan a la même taille que l'écran : un écran d'une résolution de 640 pixels par 480 demandera d'utiliser un arrière-plan de 640 pixels par 480. La mémoire pour stocker l'arrière-plan devrait donc être assez grosse pour stocker toute l'image affichée sur l'écran et pourrait servir de framebuffer. Le problème est moins critique pour les sprite, mais il se pose pour les plus gros sprite, qui demandent des mémoires assez importantes pour être stockés.
Pour résoudre ces problèmes, diverses techniques sont utilisées pour réduire la taille des sprites et de l'arrière-plan.
- Premièrement, les cartes 2D utilisent la technique de la palette indicée, afin de réduire le nombre de bits nécessaires pour coder un pixel.
- Deuxièmement, on force une certaine redondance à l'intérieur de l'arrière-plan et des sprites.
La seconde idée est que les sprites et l'arrière-plan sont fabriqués à partir de tiles, des carrés de 8, 16, 32 pixels de côtés, qui sont assemblés pour fabriquer un sprite. L'ensemble des tiles est mémorisée dans un fichier unique, appelé le tile set, aussi appelé le tilemap. Ci-contre, vous voyez le tile set du jeu Ultima VI.
Le tile set est généralement placé dans la mémoire ROM de la cartouche de jeu. Les tiles sont numérotées et ce numéro indique sa place dans la mémoire de la cartouche. L'idée est l'image ne mémorise pas des pixels, mais des numéros de tiles. Le gain est assez appréciable : pour une tile de 8 pixels de côté, au lieu de stocker X * Y pixels, on stocke X/8 * Y/8 tiles. Ajoutons à cela que les numéros de tiles prennent moins de place que la couleur d'un pixel, et les gains sont encore meilleurs. La consommation mémoire est déportée de l'image vers la mémoire qui stocke les tiles, une mémoire ROM intégrée dans la cartouche de jeu.
Un autre avantage est que les tiles peuvent être utilisés en plusieurs endroits, ce qui garantit une certaine redondance. Par exemple, un sprite ou l'arrière-plan peuvent utiliser une même tile à plusieurs endroits, ce qui impose une certaine redondance. C'était très utile pour l'arrière-plan, qui est généralement l'image la plus redondante. On y trouve beaucoup de tiles bleues identiques pour le ciel, par exemple. Pareil si une tile est utilisée dans plusieurs sprites, elle n'est stockée qu'une seule fois. Une autre possibilité était de lire certaines tiles dans les deux sens à l'horizontale, de faire une sorte d'opération miroir. Ce faisant, on pouvait créer un objet symétrique en mémorisant seulement la moitié gauche de celui-ci, la moitié droite étant générée en faisant une opération miroir sur la partie gauche. Mais cette optimisation était assez rare, car elle demandait d'ajouter des circuits dans un environnement où le moindre transistor était cher. De plus, les objets symétriques sont généralement assez rares.
Le matériel des anciennes cartes 2D était optimisé pour gérer les tiles. Son rôle était de reconstituer l'image finale en plusieurs étapes. Le tile set était généralement mémorisé dans une mémoire à part, il était parfois dans la mémoire ROM de la cartouche, mais il était parfois recopié dans une mémoire RAM intégrée dans la carte graphique pour plus de rapidité. Les registres pour les sprites et la RAM de l'arrière-plan étaient remplis de numéros de tiles et non directement de pixels. Le matériel rendait les sprites en lisant la tile adéquate, automatiquement. D'abord les circuits d'adressage lisaient les mémoires pour l'arrière-plan et les sprites, et récupéraient un numéro de tile. La tile correspondant était lue depuis la tilemap, dans une autre mémoire. Enfin, divers circuits choisissaient le pixel adéquat dans la tile à chaque cycle d'horloge.
Pour les consoles de 2ème, 3ème et 4ème génération, l'usage de tiles était obligatoire et tous les jeux vidéo utilisaient cette technique. Les cartes graphiques des consoles de jeux de cette époque étaient conçues pour gérer les tiles.
Le zoom et la rotation des sprites
modifierIl a existé des VDC qui pouvaient zoomer sur des sprites, voire les faire tourner. Le ZOOM des sprites permet de faire grossir ou de réduire la taille d'un sprite dynamiquement. L'effet de zoom pouvait servir pour générer des effets de profondeur. Par exemple, quand on veut simuler un personnage qui se rapproche de l'écran en marchant/courant, il suffit de prendre son sprite et de grossir le zoom progressivement pour donner l'illusion d'un personnage qui se rapproche. Idem pour n'importe quel sprite : plus le sprite est loin, plus on en réduit la taille. L'idée est de gérer la taille des sprites en fonction de la profondeur du sprite. L'effet donnait un rendu en pseudo-3D. On parle alors de Sprite Scaling.
Les implémentations les plus simples permettaient de faire multiplier les dimensions d'un sprite par 2, 4, 8 fois. En clair, la largeur et la longueur du sprite étaient multipliés par 2, 4, 8, etc. Il y avait possibilité de séparer zoom vertical et horizontal. Quelques VDC ne supportaient que le zoom horizontal, d'autre que en vertical, mais la plupart de ceux qui supportaient le zoom supportaient les deux.
Les implémentations plus complexes supportaient un zoom plus fluide, avec tous les intermédiaires possibles entre *1 et *X. Pour cela, le sprite devait subir des opérations d'interpolation pour avoir un rendu agréable à l'écran. Pour comprendre ce que cela veut dire : imaginez que vous souhaitez multiplier la taille d'un sprite par X. Pour multiplier par 2, 3, 4 ou tout autre entier, cela demande juste de dupliquer chaque pixel. Mais pour les valeurs fractionnaires, les choses sont plus compliquées. Vous devrez appliquer un filtre pour faire le zoom, filtre qui prend les valeurs des pixels et les mélange pour calculer les pixels finaux. Nous reviendrons sur les filtres possibles dans le chapitre sur le filtrage de texture. Vous avez bien lu : le zoom des sprites n'est pas si différent du filtrage de texture, les algorithmes utilisés pour zoomer sur les sprites et filtrer des textures sont d'ailleurs très similaires, voire identiques.
La rotation des sprites
modifierUne autre possibilité est de faire tourner les sprites. La rotation des sprites regroupe plusieurs techniques diverses, qui vont de simples effets de miroir à des techniques de rotation complexes.
La technique la plus simple est un effet de miroir, à savoir que le sprite est inversé horizontalement, il est tourné dans l'autre sens. Pour vous donner une idée de l'utilité de cette technique, imaginez que vous jouez à Mario : si vous allez à droite, Mrio sera tourné vers la droite. Mais si vous allez à gauche, le sprite sera tourné à gauche. Mais il s'agit d'un seul sprite, qui est tourné dans un sens ou l'autre avec un effet de miroir. Il est aussi possible d'inverser le sprite verticalement, pour le retourner.
Les techniques plus évoluées permettent de faire tourner un sprite, pour faire comme si on voyait un objet de biais. Un sprite rectangulaire représente un objet vu soit de profil, soit de face. Mais si on veut donner l'illusion d'un objet faisant un angle différent, il faut modifier la forme du sprite. Le sprite rectangulaire est transformé en trapèze, qui est d'autant moins proche d'un rectangle que l'angle est important. Déformer un sprite rectangulaire en trapèze est un effet de rotation général. Le tout donne un effet de perspective.
L'implémentation de la perspective est assez simple. Le sprite est une image formée de plusieurs lignes de pixels. L'idée est que la taille des lignes est d'autant plus réduite que la ligne est censée être loin. Les portions proches du sprite seront de taille normale, les autres sont réduites. Mais la technique demande cependant de multiplier la taille de chaque ligne par un coefficient, qui n'est pas forcément entier. Là encore, des algorithmes permettent de lisser les images des sprites pour les mettre à l'échelle. L'algorithme peut faire à la fois la gestion de la perspective et le zoom des sprites, et utiliser des algorithmes proches de ceux du filtrage de textures. Une implémentation complexe est celle du mode 7 de la Super Nintendo.
L'implémentation matérielle est paradoxalement assez simple. Mais la comprendre demande de faire la distinction entre les pixels du framebuffer et les pixels du sprite. Le sprite est une image qui est composée de pixels. Pour faire la distinction avec les pixels du framebuffer, nous allons appeler les pixels du sprite des texels. Terme qui est normalement utilisé pour les textures, mais il y a un lien qui sera fait dans quelques chapitres.
Placer le sprite sur l'arrière-plan demande de prendre les coordonnées de chaque texel et de faire un calcul qui donne les coordonnées finales x,Y dans le framebuffer. Sans rotation et sans zoom, le calcul est simple : on additionne la position x,y du sprite et la coordonnée du texel dans le sprite. Pour faire tourner les sprites, il faut faire des calculs impliquant des matrices (des objets mathématiques en forme de tableaux de nombre), que je ne détaillerais pas du tout. Pour le dire très rapidement, il faut juste multiplier les coordonnées du texel par une matrice qui encode à la fois la rotation et le zoom, mais aussi le placement au bon endroit sur l'écran (la translation). La matrice est calculée par le processeur, le VDC ne fait que faire la multiplication matricielle.
L'accélération matérielle du défilement
modifierLe défilement permet de faire défiler l'environnement sur l'écran, spécialement quand le joueur se déplace. Les jeux de plateforme rétro utilisaient énormément le défilement, le joueur se déplaçait généralement de gauche à droite, ce qui fait que l'on parle de défilement horizontal. Mais il y avait aussi le défilement vertical, utilisé dans d'autres situations. Peu utilisé dans les jeux de plateforme, le défilement vertical est absolument essentiel pour les Shoot 'em up !
Les cartes accélératrices intégraient souvent des techniques pour accélérer le défilement. La première optimisation est l'usage du blitter. En effet, défiler ne demande pas de régénérer toute l'image à afficher à partir de zéro. L'idéal est de déplacer l'image de quelques pixels vers la gauche, puis de dessiner ce qui manque. Pour cela, on peut utiliser le blitter pour déplacer l'image dans le framebuffer. L'optimisation est souvent très intéressante, mais elle est imparfaite et n'était pas suffisante sur les toutes premières consoles de jeu. Elle l'était sur les consoles plus récentes, ou disons plutôt : moins rétro. Les consoles moins rétros avaient des mémoires RAM plus rapides, ce qui rendait l'usage du blitter suffisante.
Mais certains VDC implémentaient une forme de défilement accéléré en matériel totalement différent. Les implémentations sont multiples, mais elles ajoutaient toutes des registres de défilement dans le VDC, qui permettaient de défiler facilement l'écran. Il faut noter que l'implémentation du défilement vertical est bien plus simple que pour le défilement horizontal. En effet, les images sont stockées dans le framebuffer ligne par ligne ! Et le défilement vertical demande de déplacer l'écran d'une ou plusieurs lignes. Les deux vont donc bien ensemble.
Le défilement vertical implémenté dans le CRTC
modifierUne technique utilise le fonctionnement d'un CRTC, couplé avec un framebuffer amélioré. Nous l’appellerons la technique du framebuffer étendu, terme de mon invention qui aide à comprendre en quoi consiste cette technique. Elle utilise cependant plus de mémoire vidéo qu'un framebuffer normal. Elle fonctionne très bien pour le défilement vertical, elle demande quelques adaptations pour le défilement horizontal.
La méthode demande d'utiliser un framebuffer beaucoup plus grand que l'image à afficher. Par exemple, imaginez qu'une console ait une résolution de 640*480, avec des couleurs sur 16 bits. L'image à afficher prend donc 640*480*2 = 600 Kilooctets. Maintenant, imaginez que la mémoire vidéo pour l'arrière-plan fasse 2 mégaoctets. On peut y stocker l’image à rendre, et ce qu'il y a hors de l'écran. Si une scène est assez petite, l'arrière-plan tient entièrement dans la mémoire vidéo, changer l'adresse de base permet de défiler l'écran sur une distance assez longue, voire pour toute la scène. L'idée est de tout simplement dire au CRTC de demander à afficher l'image à partir de la ligne numéro X !
Avec cette technique, il faut faire la différence entre le framebuffer et le viewport. Le viewport est la portion du framebuffer qui mémorise l'image à afficher à l'écran. Le framebuffer, lui, mémorise plus que ce qu'il faut afficher à l'écran, il mémorise quelques lignes en plus, voire un niveau entier ! Le pointeur de framebuffer et la résolution indiquent la position du viewport dans le framebuffer, qui monte ou descend en fonction du sens de défilement.
Pour la comprendre, prenez le cas où on souhaite défiler d'un pixel, verticalement vers le bas. La première ligne de l'image disparait, une nouvelle apparait en bas de l'image. Cas simple, un peu irréaliste, mais qui permet de bien comprendre l'idée. Pour rappel, un CRTC incorpore deux compteurs de ligne et de colonne, ainsi qu'un registre pour l'adresse de départ de l'image dans le framebuffer. L'idée est que l'adresse de départ de l'image est augmentée de manière à pointer sur la ligne suivante, en l'augmentant de la taille d'une ligne. Il ne reste plus qu'à remplir la ligne suivante, pas besoin de faire la moindre copie en mémoire vidéo ! Et défiler vers le haut demande au contraire de retrancher la taille d'une ligne de l'adresse. On peut généraliser le tout pour du défilement vertical.
L'implémentation la plus complexe demande d'ajouter un registre de défilement vertical, dans lequel on place le numéro de ligne à partir de laquelle il faut dessiner l'image. L'image en mémoire vidéo a plus de lignes que l'écran peut en afficher, le CRTC parcourt autant de ligne que ce que demande la résolution, le registre de défilement vertical indique à partir de quelle ligne commencer l'affichage. Le calcul de l'adresse prend en compte le contenu de ce registre pour parcourir le framebuffer.
Le défilement vertical infini : le warp-around des adresses
modifierLa technique précédente fonctionne bien, mais elle ne permet pas d'avoir du défilement infini, à savoir avec des maps dont la longueur n'est pas limitée par la mémoire vidéo. Mais on peut l'adapter pour obtenir du défilement vertical, en utilisant un comportement particulier du calcul des adresses. Le comportement en question est le warp-around.
Pour le comprendre, prenons l'exemple suivant. La mémoire vidéo peut contenir 1000 lignes, la résolution verticale est de 300 lignes. Si on démarre l'affichage à la 700ème ligne, tout va bien, on n'a pas besoin de warp-around. Mais maintenant, imaginons que le défilement vertical fasse démarrer l'affichage à partir de la 900ème ligne. L'affichage se fera normalement pour 100 lignes, mais la 101ème débordera hors du framebuffer, il n'y aura pas d'adresse associée. Le warp-around fait repartir les adresses à zéro lors d'un tel débordement. Ainsi, la 101ème adresse sera en fait l'adresse 0, la 102ème sera l'adresse 1, etc. En clair, si on commence à afficher l'image à la 900ème ligne, les 100 premières seront prises dans les 100 lignes à la fin du framebuffer, alors que les 200 suivantes seront les 200 lignes au début du framebuffer.
En faisant cela, on peut avoir un défilement vertical infini. Quand l'image affichée est démarrée assez loin, le début du framebuffer est libre, il contient des lignes qui ne seront sans doute pas affichées par la suite. Dans ce cas, on écrit dedans la suite du niveau, celle située après la ligne à la fin du framebuffer. Il suffit que le programmeur se charge de modifier le framebuffer de cette manière et on garantit que tout va bien se passer.
Avec cette technique, on peut avoir un défilement infini en utilisant seulement deux fois plus de mémoire vidéo que ce qui est nécessaire pour stocker une image à l'écran. Il est même possible de tricher pour utiliser moins de mémoire vidéo. Mais laissons cela de côté, tout cela est laissé à l’appréciation du programmeur.
Le défilement horizontal et vertical implémenté par le CRTC
modifierPour le défilement horizontal, il faut procéder de la même manière, mais en trichant un peu. Là encore, il y a une différence entre viewport et framebuffer, et les deux n'ont pas la même résolution. Mais cette fois-ci, outre la résolution verticale qui est plus grande, la résolution horizontale l'est aussi. Par exemple, si je prends la console de jeux NES, elle a une résolution pour l'écran de 256 par 240, alors que l'arrière-plan a une résolution de 512 par 480.
L'implémentation utilise des compteurs de ligne et de colonne séparés. L'idée est d'avoir des lignes plus longues dans le framebuffer que ce qui est indiqué dans le registre de résolution. On peut alors mémoriser une ligne plus longue que ce qui est affiché à l'écran, avec des portions non-affichées à l'écran.
L'idée est là encore d'initialiser le compteur de colonne avec une valeur qui est incrémentée d'un pixel à chaque fois qu'on défile vers la droite, décrémentée quand on va vers la gauche. Le registre pour la résolution horizontale, qui vérifie si la fin de la ligne/colonne est atteinte, est lui aussi incrémenté. La même méthode peut être utilisée pour le défilement horizontal en faisant la même chose pour le compteur de ligne. L'implémentation demande d'ajouter un registre de défilement horizontal, en plus du registre de défilement vertical, le principe derrière est le même mais pour le numéro de colonne et non de ligne.
Notons que le comportement de warp-around peut aussi être implémenté pour les adresses et compteurs de colonne. Cela permet d'avoir du défilement horizontal infini.
La fonctionnalité était disponible sur les cartes EGA, les toutes premières cartes d'affichage datant d'avant même le standard VGA. Tout était en place sur ces cartes graphiques pour implémenter la technique : une mémoire vidéo assez grande, un framebuffer potentiellement plus grand que ce qui est affiché à l'écran, une adresse de départ qu'on peut incrémenter ligne par ligne, par incréments de 1 pixel. Le logiciel devait cependant utiliser cette faculté adéquatement pour implémenter le défilement.
Maintenant, voyons les défauts des techniques précédentes. Le problème principal est que toute l'image bouge ! Or, le défilement demande généralement que certains éléments bougent, mais pas tous. Les sprites sont souvent gérés par un système de sprite matériel, ce qui fait qu'ils sont ou non concernés par le défilement et ce n'est pas tellement un problème. Mais d'autres problèmes surviennent en-dehors des sprites.
Le vrai problème vient des techniques de défilement à parallaxe. Le défilement à parallaxe utilise plusieurs arrière-plans superposés qui défilent à des vitesses différentes. Il donne une illusion de profondeur dans l'environnement. La technique précédente fait bouger le framebuffer en bloc, tout d'un coup, et ne gère donc pas le défilement à parallaxe.
De plus, la technique ne marche pas vraiment avec un rendu à base de tiles. La solution la plus simple permet de faire du défilement tile par tile, mais le résultat à l'écran est tout simplement pas fluide du tout. Du moins, c'est le cas sans modifications assez importantes de la technique de défilement, qui ne sont pas faites en hardware. Par contre, il existe des techniques logicielles pour implémenter du défilement en tenant compte des tiles, mais qui utilisent surtout des techniques logicielles. La solution à ce problème a été trouvée par John Carmack, le programmeur derrière les moteurs des jeux IDSoftware comme DOOM, Quake, Wolfenstein 3D, etc. Il a inventé la technique de l'Adaptive tile refresh qui permet justement d'avoir un défilement fluide sur des cartes sans gestion hardware du défilement.
L'accélération matérielle du tracé de ligne et de figures géométriques
modifierLa dernière optimisation du rendu 2D est le tracé de lignes et de figures géométriques accéléré en matériel. Quelques VDC incorporent cette optimisation, dont le nom est assez clair sur ce qu'elle fait. Tracer une ligne, un segment, est l'opération la plus courante sur de tels VDC, le tracé de cercles est déjà plus rare. Tracer des polygones est entre les deux, : plus rare que le tracé de ligne pur, moins que le tracé de cercles.
Les circuits de tracé de ligne
modifierL'algorithme le plus utilisé par le matériel pour tracer des lignes est l'algorithme de Bresenham. Il est très simple et s'implémente très facilement dans un circuit électronique. Il fut dite que cet algorithme utilise seulement des additions, des soustractions et des décalages, opérations très simples à implémenter en hardware. Il est de plus un des tout premiers algorithme découvert dans le domaine du graphisme sur ordinateur. Il existe de nombreuses modifications de cet algorithmes, qui vont de mineures à assez profondes. Et certaines d'entre elles sont plus faciles à implémenter en hardware que d'autres.
Le fonctionnement du circuit de tracé de ligne est le suivant. Premièrement, on précise le pixel de départ et le pixel d'arrivée en configurant des registres à l'intérieur du circuit, avec une paire de registre pour les coordonnées du pixel de départ, une autre parie pour les coordonnées du pixel d'arrivée. Le circuit dessine la ligne pixel par pixel, avec un pixel dessiné par cycle d'horloge.
Le tracé de figures géométriques
modifierPassons maintenant aux tracé de figures géométrique. Le tracé se fait là encore pixel par pixel, sur le principe. Généralement, le tracé est limité à des figures géométriques simples, que vous avez tous vu quand vous étiez au collège, en cours de maths. Tracer des carrés/rectangles/pentagones/trapèzes ou autres polygone est très simple : il suffit de tracer plusieurs lignes les unes à la suite des autres.
La vraie utilité est l'implémentation de courbes, comme des cercles, des ellipses, ou autres. L'algorithme de Bressenham peut être modifié pour implémenter des cercles, ce qui donne le Midpoint circle algorithm. D'autres extensions permettent de dessiner des ellipses, et même des courbes plus complexes comme des courbes de Bezier ou d'autres courbes assez complexes à expliquer.
Le tracé et le remplissage de figures géométriques par le blitter
modifierOutre le tracé des figures géométriques, il est aussi possible de gérer en hardware les opérations de remplissage. Cela veut dire dessiner l'intérieur d'une figure géométrique, comme remplir un carré ou un rectangle, avec une couleur uniforme. Par exemple : dessiner un carré en rouge, remplir un rectangle existant de bleu clair, etc. Le remplissage est souvent disponible pour certaines formes géométriques simples, comme des carrés ou rectangles, rarement plus. Pour le remplissage des triangles ou d'autres figures géométriques, le support matériel est encore plus rare, ne parlons même pas des cercles.
Le remplissage de rectangles est souvent réalisé par le blitter. Pour faire une comparaison, la méthode utilisée est globalement la même que celle utilisée pour lire le viewport dans le framebuffer, mais cette fois-ci réalisé par le blitter. Le viewport est remplaé par le rectangle à remplir, et la lecture du pixel à envoyer à l'écran est remplacée par l'écriture d'une couelur précisée dans un reistre.
Pour cela, on précise la couleur, la coordonnée X,Y et la largeur et la hauteur du rectangle dans des registres dédiés. Le remplissage commence à la coordonnée X,Y. L'adresse mémoire est alors incrémentée jusqu'à ce que la largeur voulue soit atteinte. On incrémente alors le compteur de ligne pour passer à la suivante, le compteur de colonne est réinitialisé avec la coordonnée X de la première colonne. Le remplissage s’arrête une fois que la hauteur voulue est atteinte.
Un exemple est le blitter des anciennes consoles Amiga, qui gère nativement des blocs en forme de rectangles, et de trapèzes dessinés à l'horizontale. Ils sont remplis en fournissant plusieurs informations : la position de leur premier pixel, une largeur qui est un multiple de 16 bits, une hauteur mesurée en nombre de lignes, un décalage qui indique de combien de pixels sont décalées deux lignes consécutives. Le décalage est ajouté une fois le compteur de colonne réinitialisé à sa valeur précédente (au démarrage de la ligne précédente).
L'organisation des circuits d'accélération 2D
modifierL'implémentation des optimisations du rendu 2D peuvent se faire soit directement dans le VDC, soit dans un circuit séparé. Le cas le plus fréquent est une intégration dans le VDC. Mais quelques consoles de jeu faisaient autrement : elles utilisaient un circuit séparé pour les optimisations du rendu 2D. Il est intéressant de donner quelques exemples réels de consoles de jeux qui utilisaient de l'accélération 2D. Nous allons voir que chaque console faisait à sa sauce.
Le Television Interface Adaptor des consoles Atari 2600
modifierUn premier exemple de carte 2D est celui d'une des toutes premières consoles Atari, notamment celle de l'Atari Video Computer System et l'Atari 2600. La carte graphique en question s'appelait la Television Interface Adaptor, abrévié TIA. Elle a une architecture foncièrement différente des autres consoles de l'époque : elle ne dispose pas du tout de mémoire vidéo et doit se débrouiller autrement ! À la place, le rendu d'une image est effectué ligne par ligne, et le TIA contient seulement de quoi mémoriser une ligne, dans quelques registres. Le processeur doit modifier ces registres entre l'affichage de deux lignes pour pouvoir afficher une image digne de ce nom. Il s'aggit d'une technique appelée le racing the beam.
Elle gère un arrière-plan très limité, stocké dans un registre. Le registre d'arrière-plan est un registre de 20 bits, qui est répliqué autant de fois que nécessaire sur tout la ligne. En clair, la ligne de l'écran est découpée en blocs de 20 pixels, chaque pixel étant codé sur un bit. Vous pourriez croire que la couleur codée dans ce registre est un simple noir et blanc, mais c'est plus compliqué. Un registre séparé contient la couleur affichée, elle-même encodée grâce à un système de palette indicé. Le registre d'arrière-plan indique, pour chaque pixel d'un bloc, si la couleur doit être affichée ou non.
Le TIA gère 5 sprites matériels, grâce à 5 registres capables chacun de mémoriser une ligne de pixel. Ces registres sont spécialisés, avec deux registres pour les avatars des joueurs, deux missiles et une balle. Ne vous fiez pas aux noms, la différence entre les 5 est la taille des sprites stocké dedans. Les sprites des avatars sont de 8 pixels de large, peuvent être dupliquées ou étendus. La balle est un sprite de même couleur que l'arrière-plan, de 1, 2, 4 ou 8 pixels de large. Les deux missiles sont identiques à la balle, sauf qu'ils ont la même couleur que le sprite du joueur associé.
Mettre à jour les registres du TIA à chaque ligne n'est pas si différent de modifier une mémoire vidéo entre deux lignes, ce qui fait que les solutions vues plus haut peuvent en théorie s'appliquer. Un défaut des consoles ATARI est que le processeur n'utilisait pas d'interruptions pour gérer l'affichage des lignes. À la place, le bus indiquait au processeur quand le TIA acceptait qu'on modifie ses registres. Le bus avait un signal RDY (READY) qui indiquait si le TIA était occupé à afficher une ligne ou non.
Un autre défaut est que le TIA ne génère pas totalement les signaux de synchronisation verticale, qui servent à indiquer à l'écran qu'il a fini d'afficher une image. La raison est qu'il n'a pas de compteur de ligne ! Il y a bien un compteur pour les colonnes, pour générer les signaux de synchronisation horizontale, mais pas plus. C'est le processeur qui doit générer le signal de synchronisation verticale de lui-même ! Pour cela, le processeur doit écrire dans une adresse dédie associée au TIA, ce qui déclenche un signal de synchronisation verticale immédiat.
Les VDC ANTIC et CTIA des consoles Atari post-2600
modifierD'autres consoles avaient un VDC très complexe, scindé en deux VDC reliés entre eux. Tel est le cas des consoles Atari 8 bits, qui ont succédé à l"Atari 2600. Elles n'utilisaient pas le VDC TIA, mais un autre VDC, plus complexe, composé de deux circuits séparés. Les deux circuits séparés sont appelés respectivement ANTIC et CTIA (Color Television Interface Adaptor). L'ANTIC gérait les copies en mémoire et correspondait à un blitter amélioré, alors que le CTIA était un VDC qui intégrait l'accélération matérielle des sprites et qui gérait aussi la palette indicée.
Le fonctionnement de l'ANTIC est assez complexe, mais on peut le voir comme une sorte de pseudo-processeur, qui exécute un programme de rendu 2D. Le programme en question est une suite d’opération de rendu 2d à exécuter dans l'ordre, qui permet de calculer l'image ligne par ligne. Il est appelé la Display List, et est stocké dans la mémoire de la console. L'ANTIC lit le programme instruction par instruction, grâce à un contrôleur DMA intégré dans l'ANTIC, qui contient un program counter interne à l'ANTIC. L'exécution d'une Display List sur l'ANTIC permet de décharger le processeur des tâches de rendu 2D, du moins dans une certaine mesure.
Une instruction de la display list permet de calculer une ligne de l'écran, en précisant comment combiner les sprites et l'arrière-plan, tous deux stockés dans la mémoire vidéo. La display list gére entre 0 et 240 instructions, ce qui limite la résolution verticale à 240 pixels. Les instructions peuvent se classer en quatre types principaux : des instructions pour écrire une ligne de pixels colorés, des instructions pour écrire une ligne de caractères, des instructions pour émettre les signaux de synchronisation de blanking, et enfin des branchements qui agissent sur le program counter de l'ANTIC. Les instructions pour afficher des pixels/caractères peuvent activer le défilement horizontal ou vertical suivant l'instruction.
Une instruction est codée sur 8 bits minimum.
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
DLI | LMS | VS | HS | Mode |
Les 4 bits de mode précisent quelle est l'instruction à utiliser, ce qui fait 16 instructions en tout, réparties dans les 4 classes mentionnées plus haut. Les bits HS et VS activent ou désactivent le défilement vertical et horizontal. Le bit LMS permet à l'instruction de préciser l'adresse où se trouvent les tableaux de données à utiliser, comme la place de l'arrière-plan en mémoire RAM, ce genre de choses. Pour cela, le bit est placé à 1 et l'adresse est dans les 2 octets qui suivent l'instruction. Le bit DLI gère les interruptions dites de Display List, ou interruptions DLI, que nous allons voir dans ce qui suit.
Les interruptions DLI sont l'équivalent sur l'ANTIC des interruptions d'horizontal blanking, déclenchées à la fin de l'affichage d'une ligne. Elles préviennent le processeur qu'une ligne entière a été affichée. Le processeur peut alors écrire en mémoire vidéo durant un court laps de temps s'il en a besoin. Il peut alors changer les sprites à la volée, ce qui permet d'afficher plus de sprites que ce que supporte le chip CTIA/GTIA. Les interruptions sont déclenchées pour certaines lignes seulement, celles où l'instruction qui affiche la ligne le précise. Si l'instruction a son bit DLI mis à 1, alors une interruption est déclenchée à la fin de la ligne. S'il est à 0, pas d'interruption. La gestion des interruptions DLI est donc totalement programmable.
Niveau registres, l'ANTIC contient un program counter et de quoi configurer le contrôler DMA. Il contient aussi deux registres pour configurer le défilement horizontal et vertical. Il a aussi des registres pour localiser les données importantes en mémoire, qui sont regroupées dans des tableaux séparés. Il y a notamment un registre pour l'adresse de l'arrière-plan, un autre pour l'adresse des sprites en mémoire RAM. Pour gérer la synchronisation avec le CPU, l'ANTIC incorpore un compteur de ligne, ainsi qu'un registre de synchronisation horizontale qui est à 1 quand une ligne est en train d'être affichée (mécanisme redondant avec les interruptions DLI). Il y a aussi plusieurs registres pour gérer les interruptions, avec un registre de statut que le CPU peut lire à loisir pour savoir quelle est la raison qui a déclenché l'interruption, ainsi qu'un registre de configuration des interruptions.
Les anciens micro-ordinateurs Amiga
modifierPour finir ce chapitre, nous allons voir les anciens ordinateurs de marque Amiga, qui servaient aussi de console de jeu. Ces consoles disposaient d'un processeur Motorola MC 68000, couplé à une mémoire RAM principale, et d'un chipset qui gère tout ce qui a trait aux entrée-sorties (vidéo, sonore, interruptions, clavier et souris, lecteur de disquette). Il contient plusieurs circuits appelés respectivement Paula, Denise et Agnus.
- Agnus gère la communication avec la mémoire vidéo et contient notamment un blitter et un co-processeur qui commande le blitter.
- Denise est une carte graphique de rendu 2D qui gère 8 sprites matériels. Il s'occupe aussi de la souris et du joystick.
- Paula est un circuit multifonction qui fait à la fois carte son, controleur du lecteur de disquette, gestion des interruptions matérielles, du joystick analogique, du port série et de toutes les autres entrées-sorties.
Anus contient un blitter, un CRTC, et un co-processeur spécialisé qu'on détaillera plus bas. Le fait qu'il intègre un CRTC fait qu'il gère aussi la génération des timings vidéos pour l'écran, le signal d'horloge du chipset, et quelques autres signaux temporels.
Agnus incorpore un circuit de gestion de la mémoire assez complexe. Il intégre notamment un contrôleur mémoire de DRAM, pour communiquer avec la mémoire, qui lui-même incorpore un circuit de rafraichissement mémoire. Tous les contrôleurs DRAM de l'époque ne géraient pas le rafraichissement mémoire, mais c'est le cas d'Agnus. Il n'a donc pas besoin de déléguer cette fonction au processeur. De plus, le circuit de gestion mémoire gère l'arbitrage de la mémoire, à savoir qu'il empêche le processeur et les circuits vidéo/audio de se marcher sur les pieds. Il privilégie les accès provenant du CRTC aux accès initiés par le blitter, l'affichage à l'écran ayant la priorité sur tout le reste. Agnus alloue un cycle sur deux au CRTC, le reste est alloué suivant les besoins, soit au CPU, soit au blitter.
Le blitter des anciennes consoles Amiga fonctionne suivant trois modes de fonctionnement : copie mémoire, remplissage de polygone, et tracé de ligne. Leurs noms sont assez parlants. Le mode de tracé de ligne permet de tracer une ligne en utilisant l'algorithme de Bresenham. Les deux autres modes se ressemblent, dans le sens où ils font des écritures en mémoire, pour remplir un bloc entier de forme rectangulaire.
Le premier mode copie un bloc de mémoire dans un autre, mais peut aussi effectuer une opération logique entre plusieurs blocs. Le blitter prend en entrée 0, 1, 2 ou 3 blocs, et écrit le résultat d'une opération logique dans un quatrième bloc. Les quatre blocs peuvent se recouvrir partiellement. Les blocs en question sont rectangulaires et sont définis par les informations suivantes : une longueur mesurée en multiples de 16 bits, une largeur exprimée en nombre de lignes, et un stride qui indique la distance entre deux lignes dans le framebuffer.
Pour ce qui est du remplissage de polygone, il s'agit du remplissage de figures géométriques simples avec une couleur uniforme. Les figures géométriques sont soit en forme de rectangles, soit des trapèzes dessinés à l'horizontale. Ils sont dessinés en fournissant plusieurs informations : une largeur qui est un multiple de 16 bits, une hauteur mesurée en nombre de lignes, un décalage qui indique de combien de pixels sont décalées deux lignes consécutives, la position de leur premier pixel. Il peut aussi remplir un polygone d'une couleur uniforme.
Enfin, Agnus contient un co-processeur spécialisé appelé Copper. Il s'agit plus d'une machine à état que d'un co-processeur, mais passons ce détail sous silence. Il exécute une display list, à savoir une liste d'instructions, un programme. Les instructions en question sont de trois type : MOV, WAIT et SKIP. L'instruction MOV écrit dans les registres de configuration du VDC, ce qui lui permet d'initialiser des transferts DMA, de modifier les sprites, etc. L'instruction WAIT attend que le l'écran en soit arrivé au rendu du pixel de coordonnée X,Y, ce qui peut remplacer partiellement les raster interrupts. L'instruction SKIP est techniquement un branchement qui passe outre certaines instructions de la display list si l'écran a dépassé le pixel de coordonnée X,Y.
Le co-processeur Copper peut modifier les registres du blitter, ce qui lui permet de démarrer des copies mémoire sans que le CPU ne soit impliqué. Sans lui, le CPU devrait configurer les registres du blitter pour initier une copie. Ici, le CPU n'a rien à faire : tout est précisé dans la display list. Copper peut modifier les registres de sprites à la volée, il suffit de coder la display list adéquate. Copper peut aussi générer des raster interrupts, encore que leur utilité soit ici moindre, vu que blitter et registres de sprites sont gérés par la display list. Mais elles sont supportées pour gérer la synchronisation CPU-mémoire vidéo.