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.

sml-tiles.png

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.

sml-tiles-detail.png

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 :

  1. VBLANK, qui est levée au début de la période appelée vertical blank,
  2. 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
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 :

  1. la plage allant de 980016 à 9C0016 (exclu),
  2. 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 :

  1. le bit BG (0) du registre LCDC 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,
  2. le registre BGP contient une palette qui est utilisée pour transformer la couleur des pixels de l'image de fond,
  3. les registres SCX et SCY 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.

Sorry, your browser does not support SVG.

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 :

  1. durant les 20 premiers cycles, il est en mode 2,
  2. durant les 43 cycles suivants, il est en mode 3,
  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 :

  1. É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 classe GameBoy pour y intégrer le contrôleur LCD ; tester le tout.
  2. Compléter la classe LcdController pour gérer correctement le dessin de l'image de fond uniquement (sans la fenêtre), et tester à nouveau.
  3. 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) :

  1. ils n'ont pas forcément quelque chose à faire à chaque cycle,
  2. 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_STATUS du 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.

  1. Méthodes read et write

    Les méthodes read et 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 AddressMap offre des constantes liées à ces différents éléments : VIDEO_RAM_START, VIDEO_RAM_END et VIDEO_RAM_SIZE pour la mémoire vidéo, et REGS_LCDC_START et REGS_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 registres LCDC, STAT et LYC 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, 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_LY de STAT doit éventuellement être mis à jour, et l'interruption LCD_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 ou LY et se chargeant de la mise à jour du bit susmentionné, et de la levée éventuelle de l'interruption.

  2. Méthodes cycle et reallyCycle

    Les méthodes cycle et reallyCycle ont un comportement similaire à celles du processeur : cycle détermine si le contrôleur LCD a quelque chose à faire durant le cycle courant et appelle reallyCycle si tel est le cas. La méthode cycle se charge aussi de gérer l'allumage de l'écran en détectant le cas où nextNonIdleCycle vaut Long.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.

  3. Test

    Une fois les méthodes read, write et 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 GameBoy 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éthode runUntil, entre la méthode cycle 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 :

  1. 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 (!),
  2. 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 :

  1. le 7e test de Blargg (07-jr,jp,call,ret,rst.gb),
  2. le jeu libre Flappy Boy de Felipe Alfonso,
  3. Tetris.

blargg-flappy-tetris.png

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 GameBoy 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

1

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.