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 |
---|---|---|
FF40 |
LCDC |
Configuration du contrôleur |
FF41 |
STAT |
État du contrôleur |
FF42 |
SCY |
Coordonnée Y de la zone de fond à afficher |
FF43 |
SCX |
Coordonnée X de la zone de fond à afficher |
FF44 |
LY |
Numéro de la ligne en cours de dessin |
FF45 |
LYC |
Numéro de ligne à comparer |
FF46 |
DMA |
Adresse de la source de copie mémoire |
FF47 |
BGP |
Palette de l'image de fond et de la fenêtre |
FF48 |
OBP0 |
Palette 0 des sprites |
FF49 |
OBP1 |
Palette 1 des sprites |
FF4A |
WY |
Coordonnée Y du coin haut-gauche de la fenêtre |
FF4B |
WX |
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 | BG |
Activation de l'image de fond |
1 | OBJ |
Activation des sprites |
2 | OBJ_SIZE |
Hauteur des sprites (8 ou 16 pixels) |
3 | BG_AREA |
Provenance des tuiles de l'image de fond |
4 | TILE_SOURCE |
Provenance des images des tuiles |
5 | WIN |
Activation de la fenêtre |
6 | WIN_AREA |
Provenance des tuiles de la fenêtre |
7 | LCD_STATUS |
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 | MODE0 |
Mode (bit 0) |
1 | MODE1 |
Mode (bit 1) |
2 | LYC_EQ_LY |
Vrai ssi LYC = LY |
3 | INT_MODE0 |
Activation de l'interruption mode 0 |
4 | INT_MODE1 |
Activation de l'interruption mode 1 |
5 | INT_MODE2 |
Activation de l'interruption mode 2 |
6 | INT_LYC |
Activation de l'interruption LYC = LY |
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 registreSTAT
.
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 |
---|---|
8000-97FF |
Images des 384 (3×128) tuiles |
9800-9BFF |
Index des tuiles de fond/fenêtre (variante 1) |
9C00-9FFF |
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 | TILE_SOURCE = 0 |
TILE_SOURCE = 1 |
---|---|---|
8000-87FF |
inaccessible | 00-7F |
8800-8FFF |
80-FF |
80-FF |
9000-97FF |
00-7F |
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 registreLCDC
dé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
BGP
contient une palette qui est utilisée pour transformer la couleur des pixels de l'image de fond, - les registres
SCX
etSCY
dé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.
1.2.8 Processus de dessin
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
.
1.2.9 Effets spéciaux
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
LcdController
qui gère tous les aspects décrits plus haut sauf le dessin de l'image proprement dit ; compléter la classeGameBoy
pour y intégrer le contrôleur LCD ; tester le tout. - Compléter la classe
LcdController
pour gérer correctement le dessin de l'image de fond uniquement (sans la fenêtre), et tester à nouveau. - Compléter la classe
LcdController
en 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 bitLCD_STATUS
du registreLCDC
.
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
read
etwrite
Les méthodes
read
etwrite
, redéfinissant celles deComponent
, 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'interfaceAddressMap
offre des constantes liées à ces différents éléments :VIDEO_RAM_START
,VIDEO_RAM_END
etVIDEO_RAM_SIZE
pour la mémoire vidéo, etREGS_LCDC_START
etREGS_LCDC_END
pour la plage dédiée aux registres du contrôleur LCD.Il faut toutefois noter que la méthode
write
doit traiter l'écriture dans les registresLCDC
,STAT
etLYC
de manière particulière :- lorsqu'une écriture dans
LCDC
provoque l'extinction de l'écran, il faut que le contrôleur LCD passe en mode 0, forceLY
à 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 bitLYC_EQ_LY
deSTAT
doit éventuellement être mis à jour, et l'interruptionLCD_STAT
levé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
LYC
ouLY
et se chargeant de la mise à jour du bit susmentionné, et de la levée éventuelle de l'interruption. - lorsqu'une écriture dans
- Méthodes
cycle
etreallyCycle
Les méthodes
cycle
etreallyCycle
ont un comportement similaire à celles du processeur :cycle
détermine si le contrôleur LCD a quelque chose à faire durant le cycle courant et appellereallyCycle
si tel est le cas. La méthodecycle
se charge aussi de gérer l'allumage de l'écran en détectant le cas oùnextNonIdleCycle
vautLong.MAX_VALUE
et l'écran est allumé.La méthode
reallyCycle
se charge d'effectuer les changements de mode et de lever, le cas échéant, les interruptions correspondantes. - Test
Une fois les méthodes
read
,write
etcycle
é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 classeGameBoy
de 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é.
2.2 Classe GameBoy
La classe GameBoy
doit être modifiée pour ajouter le contrôleur LCD au système. Cela implique de :
- créer une instance de
LcdController
et l'attacher au bus, - ajouter un appel à sa méthode
cycle
dans la méthoderunUntil
, entre la méthodecycle
du 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.png
l'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 classeGameBoy
pour 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.