Programmation avec le 6502 : vers des jeux plus évolués

Magazine
Marque
Hackable
Numéro
37
Mois de parution
avril 2021
Spécialité(s)


Résumé

Nous savons à présent comment exploiter les capacités du 6502 et du PPU de la NES afin de faire des jeux, comme le Pac-Man présenté lors du dernier article. J'espère d'ailleurs que certains d'entre vous ont essayé, et sont parvenus à améliorer ce programme, disponible sur le GitHub du magazine. Aujourd'hui, nous allons voir que les cartouches de jeux elles-mêmes peuvent renfermer des trésors d'ingéniosité électronique, permettant d'augmenter les capacités de base de la console.


Body

Si vous avez tenté d'afficher un score ou d'autres informations dans le programme Pac-Man présenté la dernière fois, vous avez dû vous confronter à un problème : où afficher ces informations pour qu'elles soient toujours visibles ? En effet, comme le décor défile (scrolle), ces informations risquent de défiler en même temps.

Pourtant, la plupart des jeux arrivent à afficher un score, un nombre de vies, un nombre d'objets collectés de manière fixe, alors que le reste du décor défile. C'est une technique que l'on retrouve même dans le premier jeu phare de la NES : Super Mario Bros. (voir figure 1).

figure-1 mario-s

Figure 1 : Super Mario Bros. à trois endroits du premier niveau.

On sait déjà que l'on peut facilement faire défiler l'intégralité du décor en écrivant dans le registre PPUSCROLL pendant la VBL (pendant que le balayage-écran est après la dernière ligne affichée et avant la première de l'écran suivant). Mais comment avoir une partie de l'écran fixe ?

Une solution pourrait être d'utiliser des sprites, qui ont des positions en fonction du coin supérieur gauche de l'écran et qui ne défilent pas. Cependant, les sprites souffrent de nombreuses limitations (64 en tout, pas plus de 8 par ligne) et ce ne serait donc pas envisageable pour les informations affichées dans Super Mario Bros.

Une autre possibilité serait de ne décaler le décor qu'après qu'un certain nombre de lignes à l'écran ont été tracées. Cela semble réalisable, puisqu'il est possible d'écrire dans le registre PPUSCROLL un peu n'importe quand, tant que l'on n'essaie pas de faire défiler verticalement (ce qui demande juste plus de précautions). Le problème est alors de savoir à quel moment écrire dans ce registre avec une synchronisation parfaite entre le CPU et le PPU (le balayage-écran, pour être précis).

Certaines cartouches de jeux un peu évoluées permettent de déclencher une interruption à une ligne précise, mais ce n'est pas le cas des cartouches de base, comme celle de la première génération.

La solution vient d'un bit particulier du registre PPUSTATUS : le bit 6 nommé « sprite-0 hit ».

1. Utilisation du « sprite 0 »

1.1 Rappels

Le fonctionnement des registres du PPU est un peu complexe. Par exemple, pour écrire dans une adresse (16 bits) du PPU, on doit spécifier les 8 bits de poids fort de l'adresse, puis les 8 bits de poids faible dans le même registre, PPUADDR ($2006). De même pour le registre PPUSCROLL ($2005), la première écriture change le décalage de l'écran en X, alors que la seconde change le décalage en Y. Aussi, pour être sûr de ne pas écrire dans l'autre sens, il existe un registre, PPUSTATUS, permettant de se recaler simplement en le lisant. Un simple LDA PPUSTATUS (ou autre opcode de lecture) permet en effet de s'assurer que la prochaine écriture dans PPUSCROLL concernera bien le défilement horizontal, quel que soit le nombre d'écritures que l'on a effectuées auparavant.

Le registre PPUSTATUS a aussi d'autres utilités, toutes accessibles en lecture. Son bit 5 est malheureusement bugué, je n'en parlerai donc pas plus ici. Son bit 7 est mis à 1 juste après le rendu de la dernière ligne de l'écran et remis à 0 juste avant le rendu de la première ligne. Cela permet de savoir si on est ou non dans la VBLank. À noter que toute lecture de PPUSTATUS remet aussi ce bit à 0.

Enfin, le bit 6 de PPUSTATUS n'est mis à 1 que dans un cas très particulier : quand le PPU rencontre un pixel opaque du sprite numéro 0 superposé à un pixel opaque du décor. Et il n'est remis à 0 qu'au début du rendu de la première ligne (de l'image suivante).

Cela va nous permettre de nous synchroniser pour savoir à quel moment changer le décalage horizontal. Par exemple, dans Super Mario Bros., le premier sprite (le numéro 0) est toujours le bas d'une pièce qui est exactement superposée à la pièce complète dessinée à côté du compteur de pièces (voir figure 2). Le jeu n'a plus qu'à détecter le moment où le PPU affiche cette partie, pour ensuite ajuster le défilement horizontal.

figure-2 mario-sprite-0-s

Figure 2 : Le sprite numéro 0 dans Super Mario Bros.

Voyons comment faire cela facilement. Dans la boucle principale, après l'appel (automatique) à la routine VBL, on doit donc tester le bit 6 de PPUSTATUS en boucle jusqu'à ce qu'il passe à 1. On pourrait donc charger le contenu de ce registre (LDA PPUSTATUS), puis isoler le bit 6 avec un AND #%01000000 et boucler avec un BEQ. Mais pour une fois, le 6502 nous propose une solution plus simple en utilisant un opcode assez souvent oublié : BIT. Cet opcode prend en paramètre une adresse (ça tombe bien), fait un AND avec le contenu actuel du registre A sans en changer le contenu, positionne le bit Z du registre des flags suivant le résultat du AND et copie les bits 6 et 7 de l'adresse dans les bits V et N du registre des flags.

Ouf ! Ça fait beaucoup de choses, mais finalement tout ce qui nous intéresse, c'est que cet opcode ne modifie que le registre des flags et qu'il copie le bit 6 de l'adresse donnée en paramètre dans le bit V des flags. On peut donc implémenter le test du « sprite 0 hit » avec juste les deux lignes suivantes :

- BIT PPUSTATUS ; copie le bit 6 de PPUSTATUS dans V
  BVC -         ; boucle tant que V est nul

Il faut cependant faire attention à ne pas faire ce test trop tôt.

En effet, ce bit n’est remis à 0 qu’au moment du rendu de la première ligne de l'écran et avec notre code assez simple, la routine de la VBL ne prend pas assez de temps, et le PPU n'a probablement pas encore commencé le rendu de l'écran quand le retour dans la boucle principale s'effectue.

On aura donc l'implémentation suivante pour notre défilement en deux parties.

Dans la VBL :

  LDA #0        ; 0n met 0 pour la valeur
  STA PPUSCROLL ;   en X et en Y
  STA PPUSCROLL ;   pour le haut de l'écran

Et dans la boucle principale du jeu :

; On commence par s'assurer que l'on n'est plus dans la VBlank,
;   en attendant que le bit 6 de PPUSTATUS repasse à 0
- BIT PPUSTATUS ; Copie les bits N (7) et V (6) de PPUSTATUS dans P (le registre des flags)
  BVS -         ;   boucle si V est un 1
 
; Puis on attend que le bit 6 de PPUSTATUS repasse à 1, ce qui indique que
;   le premier sprite a bien été détecté.
- BIT PPUSTATUS
  BVC -         ; boucle si V est à 0
 
  REPT 140     ; On attend 280 cycles pour ne pas décaler le bas de notre barre d'état
    NOP        ;   en changeant la valeur de décalage trop tôt.
  ENDR         ; (chaque NOP prend 2 cycles)
 
  LDA scroll_offset ; On charge notre décalage
  STA PPUSCROLL     ;   qui devient la valeur de scrolling en X
  LDA #0            ; Et on met 0 pour la valeur
  STA PPUSCROLL     ;   de scrolling en Y

Vous retrouverez le code complet d'un programme qui met ceci en œuvre, ainsi qu'une version compilée utilisable directement dans un émulateur dans le répertoire 01-sprite-0 du GitHub du magazine [1]. Et on peut en voir le résultat sur la figure 3 avec une barre d'état qui reste fixe et un décor qui défile afin de laisser le personnage au centre de l'écran. Le sprite 0 est le petit truc vert censé représenter un circuit imprimé, en dessous des lettres « PCBS ».

figure-3 game-sprite-0-s

Figure 3 : Mise en œuvre de la technique du « Sprite 0 Hit ».

Cette technique a l'avantage d'être ultra simple à mettre en œuvre et d'être disponible même avec les cartouches les plus simples possibles (pas besoin d'électronique particulière). Elle a cependant quelques inconvénients :

  • ça consomme un sprite, le premier sur les 64 disponibles ;
  • ça demande de savoir quand attendre, donc souvent cantonné au haut de l'écran ;
  • on doit boucler jusqu'au changement de valeur du PPUSCROLL, ce qui fait perdre des cycles CPU qui sont une ressource précieuse sur un 6502.

C'est notamment à cause de ces limitations que cette technique n'est pas si souvent utilisée que cela. Rapidement, les créateurs de jeux ont mis au point des cartouches de jeux un peu plus puissantes que la base pour implémenter non seulement ce genre de défilement, mais aussi plein d'autres techniques.

2. L'électronique d'une cartouche de base

Les cartouches de base des jeux NES étaient très très simples :

  • une ROM de 16 Ko ou 32 Ko, contenant le code et les données, accédés par le CPU via un bus d'adresses de 16 bits et un bus de données de 8 bits ;
  • Une ROM de 8 Ko, contenant les graphismes (les tuiles), accédée par le PPU via un bus d'adresses de 14 bits et un bus de données de 8 bits.

figure-4 cartouche-base-s 0

Figure 4 : Schéma de l'électronique d'une cartouche de base.

Beaucoup de jeux ont été faits avec cette électronique-là (voir figure 4), comme Super Mario Bros. justement, notamment parce que ces cartouches étaient très peu chères à produire.

Le nombre de connexions entre une cartouche et la console pourrait donc être assez faible (16 + 8 + 14 + 8 + quelques signaux de contrôle), une cinquantaine environ. Mais si on y regarde de plus près, le connecteur cartouche de la NES propose beaucoup plus de connexions qui ont été exploitées par la suite par des cartouches plus évoluées, pour des jeux nettement plus avancés.

2.1 Le bus exposé

Le connecteur entre la console et la cartouche de jeux est représenté sur la figure 5, selon la documentation que l'on trouve sur [2].

figure-5 cartridge-bus-s

Figure 5 : Le bus entre la console NES et une cartouche.

Il y a sur cette figure bon nombre de signaux que l'on peut reconnaître :

  • GND et +5V sont juste l'alimentation ;
  • SYSTEM CLK : l'horloge générale de la console (qui ne sert pas à grand chose, elle est beaucoup trop élevée) ;
  • M2 : l'horloge du CPU, ou plus exactement l’information signal ready. Cette broche est active lorsque le CPU est prêt à lire ou à écrire une donnée sur le bus correspondant. Et ceci a lieu une fois par cycle CPU ;
  • CPU A0 à CPU A14 : le bus d'adresses du CPU (sans le CPU A15). La ligne d'adresse CPU A15 est en fait accessible via la broche ROMSEL (sélection de la ROM). Ce statut un peu particulier vient du fait que, normalement, les accès à des adresses avec A15 positionné sont dans les 32 Ko supérieurs de l'espace d'adressage et sont donc la ROM du jeu ;
  • CPU D0 à CPU D7 : le bus de données du CPU ;
  • PPU A0 à PPU A13 : le bus d'adresses du PPU (avec une broche A13 qui permet de savoir qu'on adresse ou non les 8 premiers Ko) ;
  • PPU D0 à PPU D7 : le bus de données du PPU.

Et c'est à peu près tout ce qui est utilisé sur les cartouches les plus simples.

Mais certaines broches semblent très prometteuses pour des fonctionnalités avancées :

  • CPU R/W : cette broche indique si le CPU essaie de lire ou d'écrire à l'adresse indiquée par son bus d'adresses. Cela va permettre à certaines cartouches d'avoir de la RAM, volatile ou non (par exemple, pour stocker des sauvegardes !), mais aussi de devenir programmable. En effet, si un jeu écrit dans une adresse où il y a normalement de la ROM $8000 par exemple, la cartouche pourra interpréter cette écriture et modifier son comportement par la suite. Cela permet d'envisager énormément de possibilités, comme nous allons le voir.
  • IRQ : cette broche est reliée directement à la broche du même nom du 6502. Elle va permettre à la cartouche d'indiquer qu'il est temps pour le CPU d'exécuter une routine dont l'adresse est indiquée dans la ROM en $FFFE, quelle que soit l'occupation du CPU à ce moment-là. C'est ce que l'on appelle la routine d'interruption masquable. Cela ouvre également d'énormes perspectives, comme on va le voir dans la partie suivante.
  • PPU WR et PPU RD permettent à la cartouche de savoir si le PPU est en train de lire ou d'écrire et vont donc permettre de se synchroniser avec lui (par exemple, pour déclencher une interruption sur le 6502).

Les broches dont le nom commence par CIC servent à l'identification des cartouches officielles.

Signalons aussi les broches EXP0 à EXP9 qui sont directement reliées à un port d'expansion situé sous la NES, derrière un cache en plastique qu'il faut découper si on veut y accéder. Autant dire que ce port n'a jamais vraiment été utilisé.

Les autres signaux ont des utilisations encore plus ésotériques...

3. Les mappers

Les développeurs de jeux et constructeurs de cartouches ont donc tiré profit de ces signaux par l'ajout d'électronique supplémentaire, ce qui ne pose pas de véritable problème de compatibilité avec la console, et on a donc vu fleurir énormément de types de cartouches.

Cela a cependant posé un problème pour les émulateurs qui devaient être adaptés pour chaque jeu. En effet, non seulement ils devaient prendre en charge le code et les données présents dans les ROM (il existe des moyens simples et légaux d'extraire ces données, lorsque l'on possède la cartouche de jeu correspondante). Mais ils devaient aussi émuler l'électronique particulière de chaque type de cartouche.

Un fichier .nes contient donc en plus des données des ROM quelques informations, dont le type de cartouche. Ce sont ces informations que l'on retrouve dans l'en-tête de ces fichiers et que l'on a déjà rencontrées dans chacun des exemples sous cette forme :

;; L'en-tête pour les émulateurs
   DC.B "NES", $1a ; L'en-tête doit toujours commencer ainsi
   DC.B 1          ; Le nombre de boîtiers de 16 Ko de ROM CPU (1 ou 2)
   DC.B 1          ; Le nombre de boîtiers de 8 Ko de ROM PPU
   DC.B 0          ; Direction de scrolling et type de cartouche
                   ; Ici, on veut le type le plus simple (0)
                   ; avec un scrolling vertical (0 aussi)
   DS.B 9, $00     ; Puis juste 9 zéros pour faire 16 en tout

Et pour qu'un même fichier, qu'il soit issu d'une cartouche de jeu originale ou du développement d'un passionné indépendant (homebrew), puisse être utilisé sur n'importe quel émulateur, les créateurs d'émulateurs se sont mis d'accord sur le numéro de chaque type de cartouche.

Par exemple, le type le plus simple avec juste les deux ROM aura le numéro 0. Un autre type, très utilisé et que l'on va détailler sera le numéro 4, et aura en plus le doux nom de MMC3, car ce sigle était inscrit sur un des composants utilisés dans les cartouches de jeux de ce type. Pour la petite histoire, MMC signifie à la base Memory Management Controller, car le rôle principal de ce composant est de gérer de grandes quantités de mémoire et d’en présenter seulement une partie à chaque instant au CPU ou au PPU de la NES. Et même si ces circuits sont en général capables de faire plus que cela, comme gérer des interruptions, ils sont généralement sous la forme d’un ASIC (circuit spécialisé) assez simple, équivalent à une centaine de portes logiques.

On trouve ainsi des centaines (!) de types de cartouches, qui sont tous décrits dans les moindres détails sur des sites comme [3].

Et comme, du point de vue du programmeur, un type de cartouche est avant tout une association entre des adresses particulières et une action (généralement, un registre d'une partie de la cartouche), un type de cartouche dans ce contexte est appelé mapper.

On dira par exemple que tel jeu ou tel homebrew utilise le mapper MMC3 (ou le mapper 4).

Voyons justement comment fonctionne une cartouche qui implémente le MMC3.

3.1 Électronique d'une cartouche de type MMC3

Voici les caractéristiques d'une cartouche utilisant un MMC3 :

  • taille de la ROM CPU (code et données) jusqu'à 512 Ko ;
  • taille de la ROM PPU (graphismes) jusqu'à 256 Ko ;
  • possibilité d'une RAM (volatile ou non) de 8 Ko ;
  • génération d'une interruption par ligne d'écran (programmable).

On peut alors se demander comment tout cela est possible. Le 6502 avec son bus de 16 bits ne peut en effet pas adresser plus de 64 Ko. Et le PPU ne peut pas gérer plus de 8 Ko de ROM à la fois.

Les cartouches de type MMC3 permettent de gérer tout cela par un système de banques. Ce système permet d'utiliser une partie des 512 Ko par morceaux de 8 Ko ou 16 Ko et il sera possible de choisir les morceaux en « programmant » une puce supplémentaire qui se trouve sur la cartouche : le composant MMC3.

figure-6 cartouche-mmc3-s

Figure 6 : Schéma de l'électronique d'une cartouche de type MMC3.

La « puce » MMC3 est elle-même assez simple. Elle contient juste quelques registres, un compteur et quelques portes logiques.

En simplifiant un tout petit peu, afin de gérer jusqu'à 512 Ko de mémoire ROM, le MMC3 utilise 4 registres internes de 6 bits chacun.

La ROM CPU est découpée en (jusqu'à 64) morceaux de 8 Ko chacun et à chaque lecture dans la ROM (donc dans les 32 Ko supérieurs), le MMC3 intervient de la façon suivante :

  • la broche d'adresses A15 est à 1, par définition ;
  • les broches d'adresses A0 à A12 sont utilisées telles quelles ;
  • les broches d'adresses A13 et A14 permettent de choisir un des 4 registres ;
  • une adresse « virtuelle » est composée avec les 13 bits (A0 à A12), plus les 6 bits du registre sélectionné, soit 19 bits ;
  • 19 bits permettent d'adresser 512 Ko (2 puissance 19 = 512 * 1024) dans la ROM ;
  • les huit bits de données à cette adresse sont présentés directement sur le bus de la cartouche.

Cette façon de faire, très simple à mettre en œuvre, permet donc d'avoir 4 morceaux de 8 Ko (32 Ko) utilisables à chaque instant, ces 4 morceaux étant choisis parmi les 64 disponibles.

De façon similaire, la ROM du PPU est découpée en 256 morceaux de 1 Ko, dont 8 sont accessibles directement par le PPU.

Cette technique qui est utilisée jusque dans nos ordinateurs modernes s'appelle quelques fois la « translation d'adresse » ou address mapping en anglais, d'où le terme de « Mapper ».

3.2 Les registres du MMC3

Le MMC3 possède donc des registres internes. Et comme il « lit » tout ce qui passe sur les bus d'adresses et de données, il peut intercepter les cas où un programme essaie d'écrire dans une zone réservée à la ROM. Pour cela, il n'utilise que 3 lignes d'adresses : A14, A13 et A0 (les autres lignes d'adresses sont ignorées et on les laisse en général à zéro). Ces trois bits vont permettre d'indiquer ce que le MMC3 devra faire avec la donnée.

Ainsi, le mode de fonctionnement du MMC3 pourra être ajusté en écrivant des valeurs aux adresses $8000 (CTRL), $8001 (DATA), $a000 (PRGRAM), $a001 (MIRROR), $c000 (IRQ_LATCH), $c001 (IRQ_RELOAD), $e000 (IRQ_DISABLE), $e0001 (IRQ_ENABLE).

Ainsi, si on écrit dans $8000 (A14, A13 et A0 à zéro), la donnée permettra de choisir quel(s) registre(s) interne(s) de sélection de banques sera affecté par la prochaine écriture dans $8001.

Si on écrit à l'adresse $e000, quelque soit la valeur, cela désactivera l'interruption IRQ, comme on le verra dans la prochaine partie de cet article.

3.3 Gestion de l'interruption ligne

En plus de la possibilité d'accéder à beaucoup plus de mémoire, les cartouches MMC3 permettent de générer une interruption IRQ (interrupt request).

Lorsque ce signal arrive sur la patte correspondante du 6502, il appelle automatiquement une fonction que l’on nomme ISR (interrupt sub routine), dont l’adresse est donnée à l’adresse $FFFE en ROM. Dans le cas du MMC3, le signal IRQ pourra être généré lorsque le PPU aura traité un certain nombre de lignes d'écran. C'est parfait pour ce que l'on veut faire : un défilement qui ne commence qu'à une ligne donnée. Il faudra simplement prendre quelques précautions, car cette fonction, l’ISR, peut être appelée alors que le CPU fait tout autre chose.

Voyons comment modifier notre programme précédent pour utiliser ça.

Tout d'abord, il faut changer l'en-tête destiné aux émulateurs pour indiquer qu'on désire utiliser le mapper numéro 4 qui correspond au MMC3.

;; L'en-tête pour les émulateurs
   DC.B "NES", $1a ; L'en-tête doit toujours commencer ainsi
   DC.B 1          ; Le nombre de boîtiers de 16 Ko de ROM CPU (1 ou 2)
   DC.B 1          ; Le nombre de boîtiers de 8 Ko de ROM PPU
   DC.B $40|1      ; Direction de scrolling et type de cartouche
                   ; Ici on veut le type MMC3 (Mapper 4)
                   ; avec un mirroring vertical, car on veut
                   ; un scrolling horizontal (1)
   DS.B 9, $00     ; Puis juste 9 zéros pour faire 16 en tout

Puis on définit des adresses pour envoyer des réglages au MMC3, en écrivant dans des zones normalement situées en ROM :

; MMC3
MMC3_CTRL        EQU $8000
MMC3_DATA        EQU $8001
MMC3_MIRROR      EQU $a000
MMC3_PRGRAM      EQU $a001
MMC3_IRQ_LATCH   EQU $c000
MMC3_IRQ_RELOAD EQU $c001
MMC3_IRQ_DISABLE EQU $e000
MMC3_IRQ_ENABLE EQU $e001

Ce sera alors plus facile de modifier le comportement du MMC3, en utilisant ces adresses directement.

Pour mettre en place notre système de défilement uniquement après une ligne donnée, on va pouvoir utiliser l'interruption proposée par le MMC3. Il y a 4 adresses où on peut écrire pour régler son fonctionnement :

  • MMC3_IRQ_LATCH : la valeur écrite ici permet de choisir le nombre de lignes d'écran avant le déclenchement de l'interruption ;
  • MMC3_IRQ_RELOAD : une écriture ici (quelle que soit sa valeur) charge un registre interne du MMC3 avec la valeur écrite via MMC3_IRQ_LATCH. C'est ce registre qui est décrémenté à chaque ligne d'écran et déclenche l'interruption quand il passe à 0. La valeur passée à MMC3_IRQ_LATCH ne change pas et pourrait resservir ;
  • MMC3_IRQ_DISABLE : une écriture ici désactive le mécanisme des interruptions du MMC3 ;
  • MMC3_IRQ_ENABLE : une écriture ici réactive ce mécanisme.

On va donc simplement modifier notre routine VBL afin de générer une interruption après un certain nombre de lignes d'écran et remettre le défilement à 0 :

VBL:
  PHA           ; On sauvegarde A sur la pile
  [ ... ]
  ; Séquence d'initialisation de l'interruption ligne du MMC3
  STA MMC3_IRQ_DISABLE ; On désactive le temps des réglages
  LDA #34               ; On veut activer le scrolling après la 32ème ligne
                        ;   (avec un peu de marge)
  STA MMC3_IRQ_LATCH    ; Le registre Latch contiendra le numéro de la ligne
                        ;   où se déclenchera l'interruption
  STA MMC3_IRQ_RELOAD   ; On recharge l'interruption (on la réarme)
  STA MMC3_IRQ_ENABLE   ; Et on réactive le tout.
 
  LDA #0        ; Et on met 0 pour la valeur
  STA PPUSCROLL ;   en X et en Y
  STA PPUSCROLL ;   pour le haut de l'écran
  PLA           ; Récupération de A
  RTI           ; Fin de routine d'interruption

La boucle principale du programme n'a elle plus à se préoccuper de tout cela et n'en est que plus simple.

La routine d'interruption est elle aussi assez simple, puisqu'elle se contente de modifier les valeurs du défilement horizontal et de désactiver ensuite l'interruption. Notez que cette routine risque d'être appelée à n'importe quel moment, quand la boucle principale exécute autre chose. Il est donc important de sauvegarder sur la pile tous les registres qu'on utilise (uniquement A dans notre cas). Et comme il s'agit d'une routine d'interruption (comme la VBL) et pas d'un sous-programme appelé par JSR, il faut bien penser à la terminer par un RTI et pas un RTS.

IRQ:
  PHA                  ; On sauvegarde A sur la pile
  LDA scroll_offset    ; On charge notre décalage
  STA PPUSCROLL        ;   qui devient la valeur de scrolling en X
  LDA #0               ; Et on met 0 pour la valeur
  STA PPUSCROLL        ;   de scrolling en Y
  STA MMC3_IRQ_DISABLE ; Puis on désactive les IRQ pour éviter qu'elle se réenclenche
  PLA                  ; Récupération de A
  RTI

La dernière chose qui reste à faire est d'ajouter l'adresse de cette routine dans les vecteurs de la fin de la ROM :

;; Les vecteurs du 6502
  ORG $FFFA
  DC.W VBL    ; Appelé à chaque début d'image
  DC.W RESET ; Appelé au lancement
  DC.W IRQ    ; Interruption ligne

Et voilà ! C'est tout ! On obtient alors le même résultat qu'avec le programme précédent. Vous trouverez le code complet de cet exemple ainsi que le .nes correspondant dans le répertoire 02-status-bar-top du GitHub du magazine [1].

Alors certes, cette méthode nécessite de l'électronique supplémentaire, mais elle est tout aussi simple à mettre en œuvre que celle du « Sprite 0 Hit » et ne nécessite pas d'attente active. De plus, on peut sans difficulté utiliser cette technique plusieurs fois dans la même frame afin d'obtenir un défilement sur plein de niveaux, il suffit de répéter le même procédé.

On peut aussi évidemment créer une barre d'état fixe en bas de l'écran pendant que le haut défile. Cela ne demande que très peu de changements, et vous en trouverez un exemple dans le répertoire 03-status-bar-bottom du GitHub du magazine [1]. On peut en voir le résultat sur la figure 7.

figure-7 status-bar-bottom-s

Figure 7 : Notre jeu avec la barre d'état en bas.

3.4 Les banks switching

La possibilité de manipuler beaucoup de banques de code et de données permet bien évidemment de créer des programmes plus vastes et des jeux plus complets. Il serait donc long et fastidieux d'en donner un exemple dans un article tel que celui-ci.

Mais comme il est aussi possible de changer les banques graphiques, cela va nous permettre de facilement mettre en œuvre un effet très intéressant et très à la mode dans les jeux de l'époque : un défilement sur deux plans.

L'idée est d'avoir des éléments du décor qui défilent à des vitesses différentes pour donner une impression de profondeur. Cette technique nommée parallax scrolling [5] était très utilisée sur les machines de la fin des années 1990 et a agité les combats que se livraient les « démo-makers » de l’époque sur Amiga et Atari ST. En effet, contrairement à l’Atari ST, l’Amiga et les consoles plus modernes que la NES permettent de faire ça « en hard », mais il existe plein de façons de s'approcher de cet effet avec un peu d'astuce.

La méthode que je propose (probablement une des plus simples) consiste à garder la plus grande partie du décor comme avant et à avoir un motif qui se répète dans ce qui sera le second plan : ce qui défilera moins vite. Dans notre cas, on prendra un motif qui fait 8 pixels de large. Il suffit ensuite de recréer 8 fois tout le décor (et la banque graphique associée) en décalant juste le motif d'un pixel à droite à chaque fois. On peut voir le résultat de cette opération sur la figure 8.

On a alors 8 banques graphiques de 8 Ko chacune et on va utiliser la capacité de bank switching du MMC3 pour sélectionner celle qui convient à chaque instant. Par exemple, si on veut que le deuxième plan défile deux fois moins vite que le premier plan, on choisira comme banque celle dont le numéro est la moitié de la valeur de défilement du premier plan (scroll_offset), le tout modulo 8, car on n'a que 8 banques et que de toute façon au bout de 8 décalages, on revient à 0, car notre motif fait exactement 8 pixels de large.

figure-8 multi-montage-s

Figure 8 : Une partie du décor avec les huit décalages du motif du fond.

On peut faire quelques remarques, cependant. J'ai choisi une largeur de 8 pour me simplifier les dessins, on peut choisir vraiment n'importe quoi, le principe reste le même. Il y a un énorme gâchis de place, puisque seuls 32 octets changent d'une banque de 8 Ko à l'autre. On pourrait donc compacter tout cela ou en profiter pour ajouter plein d'autres effets. On pourrait aussi avoir un défilement vertical avec une vitesse encore différente.

Voyons maintenant comment le MMC3 nous permet d'indiquer quelle banque utiliser pour chaque partie de la ROM PPU. La ROM graphique de la cartouche qui peut compter jusqu'à 256 Ko est découpée en 256 morceaux de 1 Ko. Et, en simplifiant un peu, le MMC3 présente au PPU 8 Ko découpés en 2 morceaux de 2 Ko et 4 morceaux de 1 Ko. Les deux premiers (qui font donc 4 Ko) sont pour les sprites, et les 4 derniers (4 Ko aussi) sont pour les tuiles du fond. Et le MMC3 possède 6 registres internes contenant les numéros des banques dans la ROM de la cartouche.

Lors du reset, ces registres contiennent respectivement 0, 2, 4, 5, 6, 7 (les deux premiers morceaux font 2 Ko). Et c'est donc les 8 premiers Ko qui sont utilisés par défaut.

Nous voulons changer la banque qui sera utilisée pour les tuiles du fond, soit celle désignée par le registre 2 (celui qui vaut 4 par défaut, vous suivez ?). Et nous voudrons utiliser le morceau numéro 4, 12, 20, 28, etc., car nos banques de base font 8 Ko (c'est là qu'on aurait pu gagner énormément de place). On a donc la formule suivante pour la gestion du scrolling multi-plans :

registre 2 = ((scroll_offset / 2) % 8) * 8 + 4

Ce qui s'implémente assez facilement en assembleur, en plaçant ceci par exemple dans la routine VBL :

  ; Gestion du Bank switching
  LDA #2                ; On sélectionne le registre 2
  STA MMC3_CTRL         ;   (la première des tuiles du fond)
 
  LDA scroll_offset     ; On prend le décalage en cours
  LSR                   ; ... qu'on divise par deux
  AND #7                ;      pour un défilement du fond
  ASL                   ;      deux fois plus lent
  ASL                   ; Et on remultiplie par 8
  ASL
  CLC                   ; Puis on ajoute 4 pour avoir le bon
  ADC #4                ; ... numéro de banque de la ROM
  STA MMC3_DATA

Notez que rien ne nous empêche d'opérer ce bank switching à un autre moment, par exemple en plein milieu de l'écran pour des effets encore plus étonnants.

figure-9 game-bank-switching-s

Figure 9 : Notre jeu avec le défilement sur deux plans.

On peut voir le résultat de ce programme sur la figure 9, mais évidemment l'effet est nettement plus saisissant lorsque le décor du jeu défile. Je vous engage donc à récupérer le code et le fichier .nes de cet exemple dans le répertoire 04-bank-switching du GitHub [1] du magazine.

4. Autres possibilités

Les cartouches un peu plus évoluées que celles de base permettent aussi d'autres fonctionnalités :

  • ajout de RAM ou de RAM PPU (ça permet de faire de la 3D, hé oui !) ;
  • ajout de capacités musicales supplémentaires, surtout sur Famicom ;
  • système de sauvegarde avec une batterie et une RAM ;
  • etc.

Certains ont même récemment implémenté une partie du jeu DOOM en ajoutant carrément un microprocesseur dans une cartouche NES, mais bon, à ce point, la cartouche devient nettement plus puissante que la console et on se demande alors qui est le périphérique de qui.

5. La prochaine fois

Nous avons fait le tour de la programmation graphique de la NES, mais il reste un domaine que je n'ai pas du tout abordé : ses capacités sonores. Lors du prochain article, nous verrons comment tout cela fonctionnait à l'époque et là aussi, beaucoup de restrictions nous attendent !

Références

[1] Le GitHub du magazine : https://github.com/Hackable-magazine/Hackable36

[2] La description du connecteur cartouche de la NES : https://wiki.nesdev.com/w/index.php/Cartridge_connector

[3] Une bible pour tous ceux qui s'intéressent à la NES : https://wiki.nesdev.com/

[4] https://wiki.nesdev.com/w/index.php/MMC3

[5] https://en.wikipedia.org/wiki/Parallax_scrolling



Article rédigé par

Par le(s) même(s) auteur(s)

Programmation avec le 6502 : les sprites de la NES, ou comment coder le jeu Pac-Man

Magazine
Marque
Hackable
Numéro
36
Mois de parution
janvier 2021
Spécialité(s)
Résumé

Dans le précédent article, nous avons commencé à nous familiariser avec la partie graphique de la console NES (Nintendo Entertainment System). Aujourd’hui, nous allons réaliser un véritable jeu, ou du moins nous allons suffisamment le débuter pour qu’il commence à être intéressant.

Programmation avec le 6502 : découverte de la NES

Magazine
Marque
Hackable
Numéro
34
Mois de parution
juillet 2020
Spécialité(s)
Résumé

Dans les articles précédents, nous avons étudié de près le langage d'assemblage du microprocesseur 6502. Et même si j'ai essayé d'étayer le tout avec beaucoup d'exemples, tout cela est resté très théorique. Aujourd'hui, nous allons vraiment passer à la pratique en réalisant des programmes graphiques pouvant s'exécuter sur une véritable console NES ou sur un émulateur.

Programmation avec le 6502 : trigonométrons !

Magazine
Marque
Hackable
Numéro
33
Mois de parution
avril 2020
Spécialité(s)
Résumé

Lors du précédent article, nous avons parcouru les différents modes d'adressage du 6502, ce qui nous a permis d'élaborer quelques algorithmes simples, notamment pour réaliser des additions ou soustractions sur des nombres entiers de plus de 8 bits et même, des multiplications. Aujourd'hui, nous allons continuer dans cette voie en nous intéressant à la division et même aux nombres décimaux (à virgule), ce qui nous permettra de mettre un pied dans le monde effrayant de la trigonométrie !

Les derniers articles Premiums

Les derniers articles Premium

Donnez une autre dimension à vos logs avec Vector

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Avoir des informations précises et détaillées sur ce qu’il se passe dans une infrastructure, et sur les applications qu'elle héberge est un enjeu critique pour votre business. Cependant, ça demande du temps, temps qu'on préfère parfois se réserver pour d'autres tâches jugées plus prioritaires. Mais qu'un système plante, qu'une application perde les pédales ou qu'une faille de sécurité soit découverte et c'est la panique à bord ! Alors je vous le demande, qui voudrait rester aveugle quand l'observabilité a tout à vous offrir ?

Du graphisme dans un terminal ? Oui, avec sixel

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

On le voit de plus en plus, les outils en ligne de commandes s'étoffent peu à peu d'éléments graphiques sous la forme d'émojis UTF8. Plus qu'une simple décoration, cette pointe de « graphisme » dans un monde de texte apporte réellement un plus en termes d'expérience utilisateur et véhicule, de façon condensée, des informations utiles. Pour autant, cette façon de sortir du cadre purement textuel d'un terminal n'est en rien une nouveauté. Pour preuve, fin des années 80 DEC introduisait le VT340 supportant des graphismes en couleurs, et cette compatibilité existe toujours...

Game & Watch : utilisons judicieusement la mémoire

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Au terme de l'article précédent [1] concernant la transformation de la console Nintendo Game & Watch en plateforme de développement, nous nous sommes heurtés à un problème : les 128 Ko de flash intégrés au microcontrôleur STM32 sont une ressource précieuse, car en quantité réduite. Mais heureusement pour nous, le STM32H7B0 dispose d'une mémoire vive de taille conséquente (~ 1,2 Mo) et se trouve être connecté à une flash externe QSPI offrant autant d'espace. Pour pouvoir développer des codes plus étoffés, nous devons apprendre à utiliser ces deux ressources.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 76 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous