Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/07026.jsonl.gz/1137

Contrôleur d'écran
Gameboj – Étape 9
1 Introduction
Le but de cette étape est de commencer à écrire le composant simulant le contrôleur de l'écran à cristaux liquide. Ce composant est chargé de calculer l'image affichée à l'écran en combinant les images mentionnées à l'étape précédente : image de fond, fenêtre et sprites. A l'exception du processeur, il s'agit du composant le plus complexe à simuler, et sa réalisation est dès lors répartie sur deux étapes.
Avant de décrire le contrôleur lui-même, il est toutefois nécessaire d'expliquer le découpage en tuiles des images du Game Boy.
1.1 Tuiles
Comme nous l'avons vu, l'image de fond du Game Boy est une image carrée de 256 pixels de côté. Ce qui n'a toutefois pas été mentionné jusqu'à présent est que cette image — comme toutes les autres images du Game Boy — est constituée de plusieurs petites images carrées de 8 pixels de côté, nommées tuiles (tiles)1.
Etant donné que les tuiles font 8 pixels de côté, l'image de fond est composée de 32×32 tuiles. Cette organisation en tuiles est bien visible sur la figure ci-dessous, qui montre l'image de fond initiale de Super Mario Land sur laquelle a été superposé un échiquier dont les cellules — rouges et bleues — font 8 pixels de côté. Chaque case correspond donc à une tuile.
Figure 1 : Tuiles composant l'image de fond de Super Mario Land
L'image de fond peut être composée d'au maximum 256 tuiles différentes, car chaque tuile est identifiée par un octet. Etant donné que cette image comporte un total de 1 024 (32×32) tuiles, cela implique que certaines d'entre elles apparaissent plus d'une fois. Le découpage des images en tuiles impose donc une contrainte sur le contenu des images, qui doivent forcément comporter certaines répétitions, mais permet en contrepartie de réduire la taille des données nécessaires à leur description.
La nécessité de réutiliser des tuiles implique que les différents éléments constituant les images sont généralement alignés sur des multiples de 8 pixels. Par exemple, on constate bien ci-dessus que les lettres du texte se trouvant au sommet de l'image occupent chacune une tuile. Il en découle que la même tuile peut être réutilisée chaque fois qu'une lettre donnée apparaît plusieurs fois dans l'image.
Une autre exemple de réutilisation de tuiles est visible dans la partie de l'image contenant les palmiers. La figure ci-dessous montre un agrandissement des 12 tuiles la composant. L'index de chaque tuile est donné, en hexadécimal, dans son coin bas-gauche. On constate ainsi, par exemple, que la tuile d'index
31 représente un morceau de tronc de palmier, et qu'elle apparaît au total trois fois.
Figure 2 : Tuiles constituant les palmiers de Super Mario Land
Il est important de ne pas confondre la notion de tuile et la notion de sprite. Une tuile n'est rien d'autre qu'une image carrée de 8 pixels de côté qui sert de composant de base à toutes les images affichées à l'écran, y compris les sprites justement. Un sprite est toutefois plus qu'une tuile, puisqu'il possède d'autres attributs comme sa position à l'écran, comme nous le verrons à l'étape suivante.
1.2 Contrôleur d'écran
Le contrôleur d'écran à cristaux liquides (liquid cristal display controller), souvent appelé contrôleur d'écran ou contrôleur LCD, a pour fonction principale de combiner les différentes images formant l'image finale, puis de l'afficher à l'écran.
Comme d'autres composants, le contrôleur LCD se configure au moyen de différents registres exposés via le bus. En plus de ces registres, il contient deux mémoires vives, elles aussi exposées via le bus : la mémoire vidéo et la mémoire d'attributs d'objets. La première d'entre elles est présentée plus bas, la seconde le sera à l'étape suivante.
1.2.1 Registres
Le contrôleur LCD possède un total de 12 registres 8 bits visibles sur le bus à partir de l'adresse
FF40. La table ci-dessous les résume :
|Addr.||Nom||Fonction|

||Configuration du contrôleur|

||État du contrôleur|

||Coordonnée Y de la zone de fond à afficher|

||Coordonnée X de la zone de fond à afficher|

||Numéro de la ligne en cours de dessin|

||Numéro de ligne à comparer|

||Adresse de la source de copie mémoire|

||Palette de l'image de fond et de la fenêtre|

||Palette 0 des sprites|

||Palette 1 des sprites|

||Coordonnée Y du coin haut-gauche de la fenêtre|

||Coordonnée X du coin haut-gauche de la fenêtre|
Tous ces registres valent 0 au démarrage du système. Ils peuvent être lus et écrits via le bus, à l'exception de
LY et des trois bits de poids faible de
STAT, qui ne sont accessibles qu'en lecture.
L'utilité de ces différents registres est décrite soit plus bas, soit à l'étape suivante. Les registres
LCDC et
STAT sont constitués de bits ayant une signification individuelle qu'il est important de détailler.
Le registre
LCDC est constitué de 8 bits individuels qui permettent chacun de configurer un aspect du contrôleur LCD. Il est principalement destiné à être écrit par le programme exécuté par le processeur, mais peut également être lu. L'index, le nom et la fonction de ces différents bits sont résumés dans la table ci-dessous :
|Bit||Nom||Fonction|
|0||
||Activation de l'image de fond|
|1||
||Activation des sprites|
|2||
||Hauteur des sprites (8 ou 16 pixels)|
|3||
||Provenance des tuiles de l'image de fond|
|4||
||Provenance des images des tuiles|
|5||
||Activation de la fenêtre|
|6||
||Provenance des tuiles de la fenêtre|
|7||
||Activation de l'écran LCD|
Le registre
STAT est lui aussi constitué d'un certain nombre de bits individuels, à l'exception des deux de poids faible qui représentent un nombre de 2 bits appelé le mode et décrit plus bas. La table ci-dessous résume l'utilité des différents bits du registre
STAT :
|Bit||Nom||Fonction|
|0||
||Mode (bit 0)|
|1||
||Mode (bit 1)|
|2||
||Vrai ssi

|3||
||Activation de l'interruption mode 0|
|4||
||Activation de l'interruption mode 1|
|5||
||Activation de l'interruption mode 2|
|6||
||Activation de l'interruption

|7||aucun||inutilisé|
Les 3 bits de poids faible sont accessibles en lecture seule à travers le bus. Cela signifie que lorsqu'une valeur est écrite, via le bus, dans le registre
STAT, les 3 bits de poids faible de la valeur écrite sont ignorés, et leur valeur actuelle préservée.
1.2.2 Interruptions
Le contrôleur LCD a la possibilité de lever deux interruptions du processeur :
VBLANK, qui est levée au début de la période appelée vertical blank,
LCD_STAT, qui est levée pour différentes raisons, en fonction des bits 3 à 6 du registre
STAT.
Pour mémoire, ces interruptions ont été introduites à l'étape 5 et portent respectivement les numéros 0 et 1. Les conditions exactes dans lesquelles ces interruptions sont levées sont décrites à la section 1.2.8.
1.2.3 Mémoire vidéo
La mémoire vidéo (video RAM) est une mémoire vive de 8 192 octets, visible sur le bus entre les adresses 800016 et A00016 (exclu). Elle contient deux types de données : d'une part les images de la totalité des 384 tuiles disponibles, et d'autre part les index des tuiles constituant l'image de fond et la fenêtre, dont il existe deux variantes.
La mémoire vidéo est donc constituée, en gros, de trois zones différentes qui sont résumées dans la table ci-dessous et expliquées de manière détaillée plus bas.
|Plage||Contenu|

||Images des 384 (3×128) tuiles|

||Index des tuiles de fond/fenêtre (variante 1)|

||Index des tuiles de fond/fenêtre (variante 2)|
1.2.4 Tuiles
Les 6 144 octets situés au début de la mémoire vidéo et accessibles via le bus entre les adresses 800016 et 980016 (exclu) contiennent les images des 384 tuiles utilisables à un instant donné pour construire les différentes images.
L'image d'une tuile occupe un total de 16 octets. Le premier octet de chaque image contient les 8 bits de poids faible de la première ligne de la tuile, c-à-d celle se trouvant tout en haut. Le second octets contient les 8 bits de poids fort de cette même ligne. Le troisième octet contient les 8 bits de poids faible de la deuxième ligne de la tuile, et ainsi de suite.
Il faut toutefois faire attention à la correspondance entre les bits individuels de chacun de ces octets et les pixels de l'image. La convention utilisée sur le Game Boy est que le bit de poids le plus fort, c-à-d celui d'index 7, correspond au pixel le plus à gauche. Cette convention est malheureusement incompatible avec celle que nous avons utilisée à l'étape précédente pour faire correspondre les bits des trois vecteurs représentant une ligne et les pixels ! En effet, nous avions alors décidé que le bit i d'un vecteur correspondait au pixel i. Or la convention utilisée sur le Game Boy est que le bit 7 du premier octet correspond au pixel 0, son bit 6 au pixel 1, et ainsi de suite jusqu'au bit 0 qui correspond au pixel 7.
Une manière simple de rétablir la correspondance est d'inverser l'ordre des octets lus depuis la mémoire graphique avant de les placer dans les vecteurs représentant les lignes, et c'est ce que nous ferons dans le simulateur. C'est aussi la raison pour laquelle l'ordre des bits des octets de l'image de test fournie à l'étape précédente avait été inversé.
1.2.5 Tuiles de fond et de fenêtre
L'image de fond et la fenêtre sont constituées chacune de 32×32 (soit 1 024) tuiles. Chaque tuile est identifiée par un octet, ce qui implique que la description d'une image de 1 024 tuiles occupe 1 024 octets.
La mémoire graphique comporte deux plages de 1 024 octets pouvant contenir la description de l'image de fond ou de la fenêtre :
- la plage allant de 980016 à 9C0016 (exclu),
- la plage allant de 9C0016 à A00016 (exclu).
Pour l'image de fond, c'est le bit
BG_AREA (3) du registre
LCDC qui détermine laquelle de ces deux plages utiliser : s'il vaut 0, la première est utilisée, sinon la seconde l'est. Pour la fenêtre, c'est le bit
WIN_AREA (6) du registre
LCDC qui détermine laquelle des deux plages utiliser, selon la même convention.
Dans ces deux plages, chaque tuile est identifiée par un index de 8 bits, ce qui signifie que 256 tuiles différentes peuvent être utilisées pour une image. Or nous avons vu qu'un total de 384 tuiles sont disponibles dans la mémoire vidéo ! La correspondance entre les index des tuiles et les tuiles n'est donc pas évidente au premier abord.
La manière dont cette correspondance est faite dépend du bit
TILE_SOURCE (4) du registre
LCDC. Lorsqu'il vaut 0, les 256 dernières tuiles sont utilisables pour l'image de fond et la fenêtre, alors que lorsqu'il vaut 1, les 256 premières tuiles le sont.
Attention toutefois : la manière dont les tuiles sont indexées dépend également de la valeur de ce bit ! S'il vaut 0, alors les 128 premières tuiles sont désignées par les index allant de 8016 à FF16, les 128 suivantes par les index allant de 0 à 7F16. S'il vaut 1, alors les 128 premières tuiles sont désignées par les index allant de 0 à 7F16, les 128 suivantes par les index allant de 8016 à FF16.
La table ci-dessous résume ces conventions en montrant lesquelles des trois plages de 128 tuiles sont accessibles, et avec quels index, en fonction de la valeur du bit
TILE_SOURCE du registre
LCDC.
|Plage||

||inaccessible||

||inaccessible|
Cette organisation peut sembler inutilement compliquée, mais on peut supposer qu'elle a été choisie pour que les 128 tuiles de la plage intermédiaire aient un index identique indépendamment de la valeur du bit
TILE_SOURCE.
1.2.6 Image de fond
L'image de fond complète est, comme nous venons de le voir, constituée de 32×32 tuiles, ce qui correspond à une taille de 256×256 pixels. Plusieurs registres déterminent quelle partie de cette image complète est affichée à l'écran, et avec quelles couleurs :
- le bit
BG(0) du registre
LCDCdétermine si l'image de fond est visible ou non : s'il vaut 1, l'image de fond est affichée, s'il vaut 0 elle ne l'est pas,
- le registre
BGPcontient une palette qui est utilisée pour transformer la couleur des pixels de l'image de fond,
- les registres
SCXet
SCYdéterminent quelle zone de l'image de fond complète est affichée à l'écran ; ils contiennent l'index du pixel de l'image de fond complète qui apparaît dans le coin haut-gauche de l'écran.
1.2.7 Fenêtre
Tout comme l'image de fond, la fenêtre complète est constituée de 32×32 tuiles, donc de 256×256 pixels.
Elle aussi peut être activée ou désactivée au moyen d'un bit du registre
LCDC, qui est cette fois le bit
WIN (5), et ses couleurs sont transformées par la même palette que celle utilisée pour l'image de fond, à savoir celle contenue dans le registre
BGP.
Par contre, une grosse différence par rapport à l'image de fond est que la zone (éventuellement) affichée de la fenêtre commence toujours dans le coin haut-gauche de son image, car il n'existe pas d'équivalents aux registres
SCX et
SCY pour la fenêtre. En d'autres termes, même si conceptuellement l'image de la fenêtre fait 256×256 pixels de côté, seuls ceux figurant dans le rectangle de 160×144 pixels se trouvant en haut à gauche de cette image peuvent apparaître à l'écran.
Cela dit, deux registres —
WX et
WY — permettent de configurer un aspect de la fenêtre qui n'est pas configurable pour l'image de fond, à savoir la position à l'écran du coin haut-gauche de la fenêtre. Attention toutefois : pour une raison inconnue, le registre
WX contient en réalité la position à l'écran du pixel de la fenêtre d'index (7,0), et il faut donc systématiquement soustraire 7 à
WX pour obtenir la position à l'écran du coin haut-gauche de la fenêtre. Dans ce qui suit, nous écrirons
WX' pour la valeur ajustée de
WX (c-à-d
WX' = WX - 7).
Il est très important de comprendre la différence entre
SCX et
SCY (utilisés pour l'image de fond) d'un côté, et
WX' et
WY (utilisés pour la fenêtre) de l'autre :
SCX et
SCY donnent la position dans l'image de fond complète du pixel qui apparaîtra tout en haut à gauche de l'écran ;
WX' et
WY donnent la position à l'écran du pixel haut gauche de l'image complète de la fenêtre. Cette différence est illustrée dans la figure ci-dessous, qui montre comment l'image affichée à l'écran (au centre) est obtenue par combinaison d'une partie de l'image de fond (à gauche) et d'une partie de l'image de la fenêtre (à droite). Pour faciliter la compréhension, deux pixels (P1 et P2) sont nommés.
Figure 3 : Combinaison de l'image de fond et de la fenêtre
En plus de positionner la fenêtre à l'écran, le registre
WX permet également de désactiver son dessin, au même titre que le bit
WIN du registre
LCDC. En effet, si la valeur de
WX n'est pas comprise entre 7 (inclus) et 167 (exclu), c-à-d si celle de
WX' n'est pas comprise entre 0 et 160 (exclu), alors le dessin de la fenêtre est désactivé exactement comme si le bit
WIN du registre
LCDC valait 0.
Tout comme le processeur passe sont temps à exécuter les instructions du programme, le contrôleur LCD passe son temps à dessiner les images successives qui sont affichées à l'écran. C'est-à-dire que dès que le dessin d'une image est terminé, le dessin de la suivante commence, et ainsi de suite — tant et aussi longtemps que l'écran est allumé.
Le dessin d'une image complète dure 17 556 cycles. Etant donné qu'il y a 220 cycles par secondes, il faut un peu moins d'un soixantième de seconde au contrôleur pour dessiner une image. En d'autres termes, une nouvelle image est affichée à l'écran 60 fois par secondes environ.
Le dessin procède ligne à ligne, du haut en bas de l'écran. A chaque instant du dessin, le contrôleur LCD se trouve dans un mode donné, qui décrit ce qu'il est en train de faire. Il y a en tout 4 modes, qui sont :
- le mode 0, appelé horizontal blank, qui signifie que le contrôleur LCD a terminé le dessin d'une ligne mais pas encore commencé celui de la suivante,
- le mode 1, appelé vertical blank, qui signifie que le contrôleur LCD a terminé le dessin d'une image mais pas encore commencé celui de la suivante,
- le mode 2, qui signifie que le contrôleur LCD accède à la mémoire contenant les sprites afin de dessiner ceux de la ligne courante,
- le mode 3, qui signifie que le contrôleur LCD accède à la mémoire contenant les sprites et la mémoire graphique, afin de dessiner la ligne courante.
Le numéro du mode courant est stocké dans les deux bits de poids faible du registre
STAT, nommés
MODE0 et
MODE1 plus haut. Il faut noter que sur un Game Boy réel, le programme qui s'exécute sur le processeur n'a pas le droit d'accéder aux mémoires du contrôleur LCD durant les modes 2 et 3, mais nous ignorerons ce détail dans notre simulation.
Le dessin d'une ligne prend 114 cycles durant lesquels le contrôleur LCD passe par trois modes différents, selon le schéma suivant :
- durant les 20 premiers cycles, il est en mode 2,
- durant les 43 cycles suivants, il est en mode 3,
- durant les 51 cycles restants, il est en mode 0 (horizontal blank).
A noter que, sur un Game Boy réel, les durées de ces trois modes ne sont en fait pas constantes et dépendent de très nombreux détails non documentés. Toutefois, nous ferons l'hypothèse que la durée de chaque mode est celle donnée ci-dessus.
Après avoir dessiné la totalité des 144 lignes de l'écran en alternant entre les trois modes susmentionnés, le contrôleur LCD passe en mode 1 (vertical blank) et y reste durant 1 140 cycles, soit exactement le même temps que celui nécessaire au dessin de 10 lignes. Une fois ces cycles écoulés, il commence le dessin de la prochaine image, selon le même schéma.
Lorsqu'il commence le dessin de chacune des ligne, le contrôleur LCD stocke l'index de cette ligne dans le registre
LY. Cela est vrai aussi lorsqu'il se trouve en mode 1 (vertical blank), et le registre
LY prend donc des valeurs allant de 0 à 153, même si l'écran ne fait que 144 lignes de haut.
Chaque fois qu'il change le contenu du registre
LY, le contrôleur LCD compare sa valeur avec celle stockée dans le registre
LYC. Si les deux sont égales, alors il met à 1 le bit
LYC_EQ_LY (2) du registre
STAT. De plus, si le bit
INT_LYC (6) de ce même registre vaut 1, alors il lève l'interruption
LCD_STAT du processeur. Si
LY et
LYC contiennent des valeurs différentes, alors le bit
LYC_EQ_LY de
STAT vaut 0.
L'interruption
LCD_STAT peut également être levée lors d'un changement de mode. En effet, lorsqu'il entre en mode 0, 1 ou 2, le contrôleur LCD lève cette interruption si et seulement si le bit correspondant du registre
STAT (
INT_MODE0,
INT_MODE1 ou
INT_MODE2) vaut 1. Finalement, lorsqu'il passe en mode 1 (vertical blank), il lève de manière inconditionnelle l'interruption
VBLANK.
Les interruptions
LCD_STAT et
VBLANK ouvrent la porte à différents effets spéciaux du type de ceux mentionnés à l'étape précédente. Par exemple, l'interruption liée au mode 0 (horizontal blank) est levée au moment où le dessin d'une ligne est terminé. Durant les 51 cycles que dure ce mode, le programme a la possibilité de modifier les registres du contrôleur, p.ex.
SCX pour influencer la zone de l'image de fond affichée à l'écran. C'est de cette manière qu'il est possible d'afficher à l'écran une image qui n'est pas une simple portion rectangulaire de l'image stockée en mémoire.
De manière similaire, il est possible de faire en sorte que la fenêtre n'occupe pas simplement une portion du coin bas-droite de l'écran, en l'activant et la désactivant au cours du processus de dessin. Il est très important de noter que dans ce cas, lorsque la fenêtre est réactivée, son dessin reprend là où il s'était interrompu.
Par exemple, admettons que
WX' et
WY valent 0, puis que durant le dessin des 10 premières lignes, la fenêtre soit activée (c-à-d que le bit
WIN de
LCDC vaut 1). Les lignes affichées au sommet de l'écran seront les 10 lignes du haut de l'image complète de la fenêtre. Si la fenêtre est ensuite désactivée pour les 124 lignes suivantes, puis réactivée, alors la première ligne affichée après cette réactivation sera la 11e (!) de l'image complète de la fenêtre.
1.2.10 Allumage et extinction de l'écran
L'écran du Game Boy n'est allumé que si le bit
LCD_STATUS du registre
LCDC vaut 1.
Lorsque l'écran est éteint, le contrôleur LCD est en mode 0 — ce qui, au passage, n'est pas très logique — et le registre
LY vaut 0.
Dès que l'écran est allumé, le dessin de la première image débute immédiatement et, à supposer que l'écran ne soit pas à nouveau éteint avant, son calcul est donc terminé 17 556 cycles plus tard.
1.2.11 Simulation du dessin
Tout comme nous l'avons fait pour le processeur, nous simulerons les aspects importants du contrôleur LCD en faisant abstraction de ceux qui ne le sont pas dans la majorité des cas.
La simplification principale que nous ferons dans notre simulateur est que chaque ligne sera dessinée en un seul cycle, le premier du mode 3. L'avantage de procéder ainsi est que cela nous permet de dessiner une ligne d'un coup plutôt que de devoir procéder de manière plus incrémentale, p.ex. pixel par pixel.
Durant le reste du temps, le contrôleur LCD simulé sera donc assez oisif, se contentant de changer de mode au bon moment et de lever, le cas échéant, les exceptions correspondantes.
1.3 Référence
Une très bonne description du Game Boy dans son ensemble, et du contrôleur LCD en particulier, est donnée par Michael Steil dans sa présentation The Ultimate Game Boy Talk (en anglais, environ 1h). Les différents concepts expliqués plus haut y sont illustrés au moyen de nombreux exemples de jeux, qui peuvent considérablement faciliter la compréhension.
2 Mise en œuvre Java
La mise en œuvre de cette étape n'étant pas des plus simples, il est conseillé de la faire en trois phases :
- Écrire un embryon de la classe
LcdControllerqui gère tous les aspects décrits plus haut sauf le dessin de l'image proprement dit ; compléter la classe
GameBoypour y intégrer le contrôleur LCD ; tester le tout.
- Compléter la classe
LcdControllerpour gérer correctement le dessin de l'image de fond uniquement (sans la fenêtre), et tester à nouveau.
- Compléter la classe
LcdControlleren ajoutant la gestion de la fenêtre.
Les conseils de programmation donnés plus bas suivent ce découpage en trois phases.
A la fin de la première phase, il devrait déjà être possible de faire fonctionner les tests de Blargg au moyen du programme de test fourni plus bas, et de vérifier que leur sortie textuelle est correcte. A la fin de la seconde phase, il devrait être possible de vérifier que leur sortie graphique (c-à-d l'image affichée à l'écran du Game Boy) est correcte, de même que celle des jeux FlappyBoy et Tetris — voir plus bas. Il est difficile de tester à ce stade le résultat de la troisième phase, car les programmes de test à notre disposition n'utilisent pas la fenêtre tant que l'on interagit pas avec eux via le clavier, qui n'est pas encore simulé.
2.1 Classe
LcdController
La classe
LcdController du paquetage
ch.epfl.gameboj.component.lcd, publique et finale, représente un contrôleur LCD. Etant donné que ce contrôleur est un composant connecté au bus et piloté par l'horloge, la classe
LcdController implémente les interfaces
Component et
Clocked.
La classe
LcdController possède deux attributs publics, statiques et finaux donnant la largeur (
LCD_WIDTH) et la hauteur (
LCD_HEIGHT) de l'écran, en pixels. Pour mémoire, celui-ci fait 160×144 pixels.
Le constructeur de la classe
LcdController prend en argument le processeur du Game Boy auquel il appartient, de type
Cpu. Cela est nécessaire, car le contrôleur LCD lève des interruptions dans certaines conditions, comme décrit plus haut.
En dehors des méthodes
read,
write et
cycle des interfaces
Component et
Clocked, la classe
LcdController n'offre qu'une seule méthode publique. Celle-ci, nommée p.ex.
currentImage, ne prend aucun argument et retourne l'image actuellement affichée à l'écran, de type
LcdImage.
Faites bien attention à ce que cette méthode retourne toujours une image non nulle de 160×144 pixels, même si la simulation n'a pas encore été effectuée assez longtemps pour que la première image ait été dessinée. Dans ce dernier cas, l'image retournée sera simplement vide, c-à-d que tous ses pixels seront de couleur 0.
Garantir que l'image retournée par la méthode
currentImage n'est jamais nulle permet aux utilisateurs de la classe
LcdController de ne pas devoir traiter ce cas, ce qui évite beaucoup de problèmes.
2.1.1 Conseils de programmation (phase 1)
Avant de donner des conseils de programmation précis, il est important de constater deux points communs importants entre le processeur et le contrôleur LCD (simulés) :
- ils n'ont pas forcément quelque chose à faire à chaque cycle,
- ils peuvent être désactivés pour une durée indéterminée — le premier au moyen de l'instruction
HALT, le second au moyen du bit
LCD_STATUSdu registre
LCDC.
Cette similarité implique que plusieurs techniques utilisées pour simuler le processeur, en particulier l'utilisation d'un attribut contenant le prochain cycle d'activité (
nextNonIdleCycle) et le découpage de la méthode
cycle en deux (
cycle elle-même et
reallyCycle), peuvent être réutilisées ici. C'est ce que nous vous conseillons de faire.
- Méthodes
readet
write
Les méthodes
readet
write, redéfinissant celles de
Component, sont probablement les plus simples à mettre en œuvre car elles ressemblent à celles des autres composants. Pour cette étape, elles doivent donner accès aux différents registres et à la mémoire vidéo. Notez que comme d'habitude l'interface
AddressMapoffre des constantes liées à ces différents éléments :
VIDEO_RAM_START,
VIDEO_RAM_ENDet
VIDEO_RAM_SIZEpour la mémoire vidéo, et
REGS_LCDC_STARTet
REGS_LCDC_ENDpour la plage dédiée aux registres du contrôleur LCD.
Il faut toutefois noter que la méthode
writedoit traiter l'écriture dans les registres
LCDC,
STATet
LYCde manière particulière :
- lorsqu'une écriture dans
LCDCprovoque l'extinction de l'écran, il faut que le contrôleur LCD passe en mode 0, force
LYà 0, et se place en attente indéterminée (
nextNonIdleCycle = Long.MAX_VALUE),
- lorsqu'une écritures est faite dans
STAT, les 3 bits de poids faible ne doivent pas être modifiés,
- lorsqu'une écriture est faite dans
LYC, le bit
LYC_EQ_LYde
STATdoit éventuellement être mis à jour, et l'interruption
LCD_STATlevée sous certaines conditions (décrites plus haut).
Pour gérer ce dernier cas, nous vous conseillons d'écrire une méthode privée permettant de modifier
LYCou
LYet se chargeant de la mise à jour du bit susmentionné, et de la levée éventuelle de l'interruption.
- lorsqu'une écriture dans
- Méthodes
cycleet
reallyCycle
Les méthodes
cycleet
reallyCycleont un comportement similaire à celles du processeur :
cycledétermine si le contrôleur LCD a quelque chose à faire durant le cycle courant et appelle
reallyCyclesi tel est le cas. La méthode
cyclese charge aussi de gérer l'allumage de l'écran en détectant le cas où
nextNonIdleCyclevaut
Long.MAX_VALUEet l'écran est allumé.
La méthode
reallyCyclese charge d'effectuer les changements de mode et de lever, le cas échéant, les interruptions correspondantes.
- Test
Une fois les méthodes
read,
writeet
cycleécrites, en ignorant tous les aspects liés au dessin de l'image, il est conseillé de tester une première fois votre code, ce qui implique tout d'abord de compléter la classe
GameBoyde la manière décrite à la §2.2.
Cela fait, utilisez le programme de test donné plus bas pour exécuter tous les tests de Blargg et vérifier que leur sortie textuelle, c-à-d celle affichée sur la console par le programme de test, est correcte.
2.1.2 Conseils de programmation (phase 2)
Une fois la phase 1 terminée et testée, vous pouvez passer à la phase 2, dédiée au dessin de l'image à afficher à l'écran — pour l'instant composée uniquement de l'image de fond.
Le dessin de l'image à afficher à l'écran se faisant ligne après ligne, il est conseillé d'ajouter à la classe un attribut, nommé p.ex.
nextImageBuilder, contenant un bâtisseur d'image.
Un nouveau bâtisseur est stocké dans cet attribut au début du dessin de chaque image, c-à-d lorsque le contrôleur entre en mode 2 pour la première ligne.
Lorsque le contrôleur a terminé le dessin de l'image à afficher, c-à-d lorsqu'il entre en mode 1, l'image en cours de construction dans le bâtisseur est effectivement construite et stockée dans l'attribut donnant l'image courante, retourné par la méthode
currentImage.
Le calcul de la prochaine ligne à ajouter au bâtisseur d'image peut être confié à une méthode séparée, nommée p.ex.
computeLine. Son but est de calculer la ligne d'index qu'on lui passe, en combinant l'image de fond, la fenêtre et les sprites. Pour cette phase, elle ne se charge toutefois que de l'image de fond. Cette méthode est appelée chaque fois que le contrôleur entre en mode 3.
Notez que l'interface
AddressMap définit les tableaux
TILE_SOURCE et
BG_DISPLAY_DATA qui contiennent respectivement les adresses de début des plages contenant les images des tuiles et celles des plages contenant la description des images de l'image de fond et de la fenêtre. Pensez à les utiliser pour clarifier votre code !
A ce stade, il est conseillé de tester une seconde fois votre code, en vérifiant que les images produites par le programme de test donné plus bas sont celles attendues.
2.1.3 Conseils de programmation (phase 3)
Une fois la phase 2 terminée et testée, vous pouvez passez à la phase 3, dédiée au dessin de la fenêtre.
Le gros du code à rajouter doit l'être dans la méthode
computeLine, et il faut noter que passablement de ce code peut être partagé avec celui gérant l'image de fond. En effet, plusieurs aspects de la gestion de ces deux images, en particulier la manière dont elles sont construites à partir des tuiles les composant, sont similaires et méritent d'être extraits dans une méthode auxiliaire.
Rappelez-vous que la fenêtre est désactivée lorsque le bit
WIN du registre
LCDC vaut 0, mais aussi lorsque
WX' n'est pas compris entre 0 et 160 (exclu).
Comme décrit à la §1.2.9, lorsque le dessin de la fenêtre est interrompu durant une partie du processus de dessin, il reprend là où il s'était arrêté. La manière la plus simple de gérer cela consiste à ajouter un attribut à la classe du contrôleur, nommé p.ex.
winY, remis à 0 au début du processus de dessin et incrémenté chaque fois qu'une ligne de la fenêtre est dessinée. Sa valeur est utilisée pour déterminer quelle ligne de l'image complète de la fenêtre dessiner à un moment donné.
La classe
GameBoy doit être modifiée pour ajouter le contrôleur LCD au système. Cela implique de :
- créer une instance de
LcdControlleret l'attacher au bus,
- ajouter un appel à sa méthode
cycledans la méthode
runUntil, entre la méthode
cycledu minuteur et celle du processeur,
- ajouter une méthode d'accès, nommée p.ex.
lcdController, permettant d'obtenir le contrôleur LCD.
Attention : il est important d'attacher le contrôleur au bus au moyen de sa méthode
attachTo, et pas au moyen de la méthode
attach de bus. Cela est nécessaire car à l'étape suivante, la méthode
attachTo de
LcdController sera redéfinie de la même manière que celle de la classe
Cpu l'a été, afin que le contrôleur LCD ait accès au bus.
2.3 Tests
A la fin de cette étape, le système est assez complet pour pouvoir simuler facilement différents programmes, entre autres les tests de Blargg et certains jeux. Le programme ci-dessous, qui combine des éléments de ceux des étapes 6 et 8, constitue un bon point de départ pour effectuer des tests.
Tout comme le programme de test de l'étape 6, il accepte deux arguments sur la ligne de commande : le nom d'un fichier ROM à charger, et le nombre de cycles durant lequel effectuer la simulation. Après avoir créé un Game Boy avec le contenu du fichier ROM, il exécute la simulation jusqu'au cycle donné, au moyen de
runUntil. Notez que contrairement au programme de test de l'étape 6, celui-ci ne lève pas l'interruption
VBLANK tous les 17 556 cycles, car cela est fait désormais par le contrôleur LCD.
Une fois la simulation terminée, le programme fait deux choses :
- il affiche sur la console les index des 20×18 tuiles en haut à gauche de l'image de fond, obtenus de la zone débutant à l'adresse 980016 et interprétés comme des caractères (!),
- il écrit dans le fichier nommé
gb.pngl'image affichée à l'écran à la fin de la simulation.
Interpréter les index de tuiles comme des caractères avant de les afficher peut sembler très étrange, mais s'avère fort utile avec les tests de Blargg. En effet, leur auteur a eu la bonne idée d'indexer les tuiles correspondant aux différents caractères au moyen de leur code ASCII, ce qui signifie qu'en interprétant ces index de tuiles comme des caractères, on sait quelles lettres sont affichées à l'écran !
public final class DebugMain2 { private static final int[] COLOR_MAP = new int[] { 0xFF_FF_FF, 0xD3_D3_D3, 0xA9_A9_A9, 0x00_00_00 }; public static void main(String[] args) throws IOException { File romFile = new File(args[0]); long cycles = Long.parseLong(args[1]); GameBoy gb = new GameBoy(Cartridge.ofFile(romFile)); gb.runUntil(cycles); System.out.println("+--------------------+"); for (int y = 0; y < 18; ++y) { System.out.print("|"); for (int x = 0; x < 20; ++x) { char c = (char) gb.bus().read(0x9800 + 32*y + x); System.out.print(Character.isISOControl(c) ? " " : c); } System.out.println("|"); } System.out.println("+--------------------+"); LcdImage li = gb.lcdController().currentImage(); BufferedImage i = new BufferedImage(li.width(), li.height(), BufferedImage.TYPE_INT_RGB); for (int y = 0; y < li.height(); ++y) for (int x = 0; x < li.width(); ++x) i.setRGB(x, y, COLOR_MAP[li.get(x, y)]); ImageIO.write(i, "png", new File("gb.png")); } }
En exécutant ce programme en lui passant le fichier ROM du 7e test de Blargg et 30 millions de cycles, on obtient le texte suivant dans la console (un certain nombre de lignes vides ont été supprimées pour alléger la présentation) :
+--------------------+ |07-jr,jp,call,ret,rs| |t | | | | | |Passed | | | …… | | +--------------------+
Notez que même si le texte affiché par le programme ci-dessus lors de l'exécution des tests de Blargg est identique à celui qui était affiché par le programme de l'étape 6, il est obtenu de manière totalement différente ! En effet, le programme de l'étape 6 attachait au bus un composant (de type
DebugPrintComponent) affichant les écritures faites à l'adresse FF0116, alors que le programme ci-dessus affiche le contenu de la mémoire vidéo à la fin de la simulation.
En dehors des tests de Blargg, vous devriez être capables de simuler l'exécution de programmes plus intéressants, en particulier quelques jeux. Pour ces derniers, la sortie textuelle ne suffit toutefois plus — et est en général incompréhensible. Il faut dès lors avoir terminé au moins la phase 2 décrite plus haut avant de pouvoir les exécuter, puis visualiser l'image écrite dans le fichier
gb.png.
La figure ci-dessous présente côte à côte les images générées par le programme de test donné plus haut après simulation de 30 millions de cycles pour trois fichiers ROM différents. De gauche à droite, il s'agit de :
- le 7e test de Blargg (
07-jr,jp,call,ret,rst.gb),
- le jeu libre Flappy Boy de Felipe Alfonso,
- Tetris.
Figure 4 : Images générées par le 7e test de Blargg, FlappyBoy et Tetris
Le fichier ROM de Flappy Boy est disponible au téléchargement sur github. Celui de Tetris l'est sur le site LoveROMs, mais il faut noter qu'il n'est pas évident qu'une personne ne possédant pas la cartouche du jeu ait légalement le droit de le télécharger.
3 Résumé
Pour cette étape, vous devez :
- commencer à écrire la classe
LcdController(ou équivalent) en fonction des indications données plus haut, puis augmenter la classe
GameBoypour ajouter un contrôleur LCD au système,
- tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.
Notes de bas de page
La documentation officielle du Game Boy utilise le terme caractère (character) pour nommer les tuiles. Nous préférons le terme de tuile, d'une part car il est standard, et d'autre part car « caractère » fait penser que ces images représentent des lettres, ce qui n'est (en général) pas le cas.