Elrindel/Flob

  • Accueil
  • Github
  • Liens
  • Bin Converter

Suite SHA1 sur GameBoy : Interface graphique

Publié le : 02 May 2020 Catégories : gameboy

SHA1 GUI GameBoy

Cet article est la suite de mon premier article sur l’implémentation SHA1 sur Gameboy.

Ici nous allons voir comment créer une interface graphique afin d’utiliser l’algorithme SHA1.

L’objectif est donc de permettre la saisie d’un texte et de réaliser l’affichage du hash SHA1 pour le texte renseigné.

Ecrire un texte sur GameBoy

On pourrait penser qu’il s’agit d’une fonctionnalité intégrée par défaut dans la GB mais ce n’est pas le cas !

Son écran de 160x144 pixels est divisé en 360 sections de 8x8 pixels qui font chacune référence à une tile, donc nous avons un affichage de 20x18 tiles.

Une tile est une image de 8x8 pixels. Chaque pixel nécessitant 2 bits afin de correspondre à l’une des 4 couleurs possibles, on se retrouve donc avec 16 octets (128 bits) par tile.

Ces tiles sont chargées en mémoire dans la partie VRAM, à partir de l’adresse 0x8000 ou 0x8800 suivant le bit 4 que vous aurez définit à l’adresse 0xFF40 : Voir les spécifications

Vous l’aurez peut-être déjà compris, si on veut afficher du texte, il nous faut des tiles qui correspondent aux caractères qu’on souhaite afficher !

Choisir la police d’écriture

Etrangement, il s’avère un peu compliqué de trouver des packs de tiles de caractères pour GB.

Après quelque recherches je suis arrivé sur ce site : Pixel Font Edit
Ne souhaitant pas installer l’application, j’ai simplement récupéré les fichiers .pf présents dans l’archive d’installation, ils sont disponibles sur mon GitHub à cette adresse : GameBoy Tiles Fonts

Ces fichiers .pf contiennent 256 caractères mais sous un format différent des tiles pour la GameBoy.
J’ai donc préparé un petit script de conversion :

#!/usr/bin/env python3

import getopt, sys

def usage():
    print("""
Usage : font.py [OPTIONS] <FontFile>

Options :
-o  Output file
-s  Start offset
-l  Limit tiles (number of tiles to export)
""")
    sys.exit(2)

def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:],"o:s:l:h",["output", "start", "limit", "help"])
    except getopt.GetoptError as error:
        print(error)
        usage()

    if len(args) != 1:
        usage()

    start = 0
    limit = -1
    out = None
    for opt, arg in opts:
        if opt in ("-h", "--help"):
            usage()
        elif opt in ("-s", "--start"):
            start = int(arg)
        elif opt in ("-l", "--limit"):
            limit = int(arg)
        elif opt in ("-o", "--output"):
            out = arg
        else:
            usage()

    if not out:
        usage()

    reader = open(args[0], "rb")
    writer = open(out, "wb")
    if start > 0:
        reader.seek(start*8)
    while True:
        if limit == 0:
            break
        fontChar = reader.read(8)
        if not fontChar or len(fontChar) < 8:
            break
        
        tileChar = b''
        for y in range(0, 8):
            tileCharCode = 0
            for x in range(0, 8):
                tileCharCode <<= 1
                if (fontChar[y]>>(7-x))&1:
                    tileCharCode |= 1
            tileChar += bytes([tileCharCode&0xFF, tileCharCode&0xFF])
        writer.write(tileChar)
        limit -= 1

    reader.close()
    writer.close()

if __name__ == "__main__":
    main()

J’ai fait en sorte de pouvoir sélectionner le caractères de départ (option -s) ainsi que le nombre de caractères (option -l) afin de générer facilement les tiles dont on a besoin sans pour autant convertir les 256 caractères.

Exemple d’utilisation : pftotiles.py -s 24 -l 104 -o TINYTYPE.bin TINYTYPE.pf
Permet de convertir 104 caractères à partir du 25ème caractères (l’offset commence à zéro).

Ajouter les tiles au programme

Maintenant qu’on a notre fichier de tiles, il faut l’insérer dans le code :

.ORG $0800
tiles:
  .INCBIN "TINYTYPE.bin"

On inclut ces données à l’adresse 0x0800.
Je n’ai pas trouvé d’indications concernant cette adresse en particulier, je suppose donc qu’on peut l’inclure à n’importe quelle adresse libre dans la ROM (donc après le programme) : Répartition de la mémoire

Pour ce projet, l’adresse 0x0800 est bien libre donc je vais l’utiliser.

Désormais on peut charger ces tiles dans l’espace mémoire réservé aux tiles pour l’affichage (VRAM) :

  ld de,104*16                ;de = 1664
  ld hl,$8000+$180            ;hl = $8180
  ld bc,tiles                 ;bc = tiles
loadTile:
  ld a,(bc)                   ;a = (bc)
  ldi (hl),a                  ;(hl) = a, hl += 1
  inc bc                      ;bc += 1
  dec de                      ;de -= 1
  ld a,e                      ;a = e
  or d                        ;a |= b
  jr nz,loadTile              ;Si a != 0 : Jump loadTile

Lorsque j’ai généré mon fichier de tiles (voir le chapitre précédent) j’ai limité à 104 caractères, chaque tile faisant 16 octets, c’est pourquoi je charge 1664 octets.

Concernant l’adresse de départ (0x8180), l’explication est très similaire.
Lors de la génération des tiles j’ai décidé de commencer au 25ème caractère (donc un offset de 24), ce qui fait un décalage de 24x16 octets (384 = 0x180).
Voir le chapitre suivant pour plus de détails sur ce placement !

Utiliser directement une chaine de caractères !

Comme on l’a vu, sur la GB les caractères sont des tiles qu’on doit charger nous même en mémoire. On peut donc les ranger comme on veut !

La meilleure solution pour faciliter au maximum l’utilisation de ces tiles est donc de les faire correspondre à l’ordre qu’on a l’habitude de manipuler en informatique, c’est à dire l’ASCII !
Ca tombe bien puisque c’est déjà l’ordre des caractères dans les polices d’écriture 8x8 que nous avons vu ci-dessus.

Chaque tile possède un numéro, on peut donc faire coïncider ce numéro à la valeur ASCII du caractère.

Mon caractère de référence (mon point de départ) est l’espace (\x20), les caractères précédents étants moins facilement identifiables. On doit donc placer la tile du caractère espace en position 0x20.
Les tiles commencent à l’adresse 0x8000 et chaque tile occupe 16 octets. L’adresse de la tile 0x20 est donc 0x8000 + 16 * 0x20 ce qui fait 0x8200.

On sait à quelle adresse placer le caractères espace, mais nous avons d’autres tiles avant l’espace dans le fichier que nous avons généré.
Il suffit donc de soustraire ces tiles à l’adresse pour savoir à partir de quelle espace mémoire il faudra insérer nos tiles.

J’ai 8 caractères avant l’espace dans mon fichier, je dois donc retirer 8 * 16 octets à l’adresse de l’espace : 0x8200 - 8 * 16 = 0x8180

Maintenant que nos tiles sont bien placées en mémoire, nous pouvons stocker nos chaines de caractères directement dans la ROM tout comme on l’a fait précédemment pour le fichier des tiles :

.ORG $0800
tiles:
  .INCBIN "TINYTYPE.bin"
strText:
  .DB "Text :"
strHash:
  .DB "SHA1 :"
strPressUpDown:
  .DB $18, "/", $19, " : Change char"
strPressA:
  .DB "A   : Select char"
strPressB:
  .DB "B   : Deselect char"
strPressStart:
  .DB "Press START to hash"

J’utilise .DB pour définir mes textes, et dans le cas des caractères spéciaux j’ai préféré utiliser le code hexa directement (pour les flèches up et down dans strPressUpDown).

Je peux désormais atteindre mes chaines de caractères facilement dans mon code en utilisant les labels !

Comment afficher les tiles à l’écran

L’affichage est directement lié au contenu des espaces mémoire des 3 différentes couches graphiques (Background, Window, Sprites à voir ci-dessous).

Seulement il n’est pas possible d’écrire des données n’importe quand dans ces espaces mémoire !
En effet, il faut attendre que l’écran soit éteint ou que l’affichage n’utilise plus la mémoire vidéo.
L’interruption V-Blank est justement là pour ça !
D’ailleurs, si vous souhaitez éteindre l’écran, il faut absolument le faire durant le V-Blank sinon vous risquez de réduire la durée de vie de votre écran !

Interruption V-Blank

L’interruption V-Blank permet donc de manipuler la mémoire vidéo des 3 couches mais il faut également prendre en compte une informations importante : Vous avez seulement 4560 cycles à partir du début du V-Blank

Cette limitation de 4560 cycles vient du fait que l’affichage ne va pas attendre la fin des instructions pour s’actualiser ! Ce qui a pour effet de bloquer l’accès à la mémoire durant l’affichage et donc durant le V-Blank si vous dépassez cette limite.
Donc si vous dépassez cette limite sans en tenir compte et que vous tentez d’écrire dans la mémoire vidéo, vos données ne seront pas écrites et donc pas affichées/actualisées, mais votre code continuera de s’exécuter, vous constaterez donc des trous dans votre affichage !

Pour éviter cela, il faut compter les cycles ! Chaque instruction assembleur nécessite un certain nombre de cycles pour être exécutée : Voir les instructions CPU

Toutes les informations sont disponibles dans les spécifications : Accessing VRAM and OAM
On remarque notamment un exemple permettant d’attendre l’accès à cette mémoire. Ce qui permet par exemple d’avoir un V-Blank de plus de 4560 cycles en découpant le code en plusieurs parties tout en attendant l’accès mémoire entre chaque partie (ou même permettre d’accéder à cette mémoire en dehors de l’interruption VBlank).
Dans le cas présent, nous le verrons plus bas, j’ai compté les cycles pour segmenter mon affichage en plusieurs parties.

Il existe donc 3 couches pour afficher des tiles, chacune a ses particularités :

Background

D’une dimension de 32x32 tiles, il est principalement utilisé pour afficher le niveau du jeu grâce à sa propriété qui est d’être scrollable, c’est à dire que la zone affichée à l’écran (20x18 tiles) peut se balader sur le background.
De plus, le scroll dans le background est infini puisqu’il peut tourner en boucle ! Le bas est lié au haut tout comme le bord droit est lié au bord gauche.

La background est stocké à partir de l’adresse 0x9800 ou 0x9C00 suivant le bit 3 définit à l’adresse 0xFF40 : Voir les spécifications

Chaque octet du background correspond au numéro de la tile à afficher.

Window

Cette couche vient par dessus le background. Elle possède également une dimension de 32x32 tiles (1 octet par tile) mais n’est pas scrollable comme le background.
Sa propriété est de pouvoir être déplacée sur l’écran, ce qui permet par exemple de créer une boite de dialogue, un menu etc…

Tout comme le background, son adresse est soit 0x9800 soit 0x9C00 dépendamment du bit 6 définit à l’adresse 0xFF40.

Sprites

Les sprites ont la priorité d’affichage la plus haute (ils sont affichés au dessus des autres couches).
Il est possible d’afficher un maximum de 40 sprites mais avec une limitation à 10 sprites par ligne.

Un sprite est composé de 1 tile (8x8 ou 8x16 suivant le flag), de coordonnées X et Y pour le positionner sur l’écran, ainsi que d’un flag correspondant à quelques options : VRAM Sprite Attribute Table (OAM)

Il y a donc 4 octets par sprite (Y, X, tile, flag). Ils sont stockés à partir de l’adresse 0xFE00.

On peut utiliser les sprites pour un très petit texte ou pour afficher les personnages dans le jeu par exemple.

Choix de l’affichage pour ce projet

Vu la quantité d’informations à afficher, j’élimine les sprites.

Il reste donc background ou window. Les deux peuvent répondre au besoin !

Le background est très simple à manipuler, j’ai donc décidé de l’utiliser.

Exemple d’affichage d’un texte

Voici un exemple pour afficher le texte Press START to hash sur la dernière ligne de l’écran :

  ld hl,$9A20
  ld bc,strPressStart
  ld d,19
loopStrPressStart:
  ld a,(bc)
  ldi (hl),a
  inc bc
  dec d
  jr nz,loopStrPressStart

L’adresse du background commence à 0x9800, chaque ligne du background possède 32 positions, le scroll est placé en 0,0 et l’écran a 18 lignes. On peut donc calculer l’emplacement de la dernière ligne affichée à l’écran : 0x9800 + 17 * 32 = 0x9A20
(17 car on veut le début de la 18ème ligne et non le début de la 19ème, tout comme il faudrait mettre 0 pour avoir le début de la première ligne)

Il faut évidemment spécifier la longueur du texte (19 dans le cas présent).
Sachant que l’écran permet d’afficher 20 caractères par ligne, si le texte est plus long il faudra donc l’afficher sur plusieurs lignes.

Détection des touches

La lecture de l’état des boutons n’est pas compliquée mais s’avère être potentiellement instable !

En effet, seuls 4 bits (0 à 3) du registre 0xFF00 permettent de récupérer l’état des boutons.
Mais la GameBoy possède 8 boutons, donc ces 4 bits sont partagés suivant l’état des bits 4 et 5 comme spécifié dans la doc : Joypad Input

Notez bien que les bits 4 et 5 sont sélectionnés lorsqu’ils sont à 0 et que les bits des boutons sont également à 0 lorsque ces derniers sont appuyés.

Donc, pour lire l’état des boutons, il faut procéder en deux étapes :

  1. On définit les bits 4 et 5 dans le registre 0xFF00 afin de spécifier quelles touches on souhaite lire
  2. On récupère la valeur de ce même registre (0xFF00) pour lire l’état des bits 0 à 3 correspondants aux boutons

Concernant l’instabilité que j’évoquais précédemment, elle est liée au fait que l’attribution des bits correspondants aux états des boutons n’est pas instantanée !
La lecture peut donc être faussée si elle est faite trop rapidement.

Dans la pratique, il est donc recommandé de lire plusieurs fois le registre 0xFF00 pour être certain de récupérer les bonnes valeurs. On peut également définir les bits 4 et 5 en amont dans le code afin d’anticiper la future lecture !

Voici un exemple pour la touche Down :

  ld a,%00100000            ;a = 0b00100000
  ldh ($00),a               ;(FF00) = a
  
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  
  bit 3,a                   ;Test bit 3 du registre a
  jr nz,notKeyDown          ;Si bit 3 != 0 : Jump notKeyDown
  ;TODO : action key down
notKeyDown:

On définit les bits 4 et 5 du registre 0xFF00 pour sélectionner les touches de direction (bit 4 à 0 et bit 5 à 1).

Afin de bien lire la bonne valeur, on fait plusieurs lectures du registre 0xFF00.
Je n’ai pas trouvé d’informations précises sur le nombre de cycles nécessaires avant d’être vraiment certain de la lecture … à l’occasion je procèderai à quelques tests pour déterminer cela.

Et enfin, on test le bit de la touche souhaitée, si il n’est pas à 0 on saute après le traitement du bouton.

Dans l’optique d’optimiser le nombre d’instructions dans l’interruption VBlank, je place la gestion des touches dans la boucle principale loop !

Saisie d’un texte depuis la GameBoy

Maintenant qu’on sait comment afficher du texte à l’écran et comment détecter l’appui d’une touche, on va pouvoir mettre en place le système de saisie de caractères.

L’écran pouvant afficher 20 tiles sur une ligne, j’ai décidé de limiter la saisie à 20 caractères, ce qui est déjà pas mal pour tester le SHA1 !

Espace mémoire nécessaire

Voici les variables nécessaires pour réaliser la saisie d’un texte :

VBlankTimer  DB         ;Compteur VBlank pour avoir un timer synchronisé sur l'affichage
KeyLastDir   DB         ;Dernière touche de direction appuyées
KeyLastBut   DB         ;Dernier bouton appuyé
CharTmp      DB         ;Caractère en cours
CharCount    DB         ;Nombre de caractères déjà sélectionnés
CharBuffer   DS 20      ;Enregistrement des caractères sélectionnés

VBlankTimer sera nécessaire pour gérer le clignotement du caractère en cours, voir ci-dessous.

Les deux variables KeyLast permettent d’éviter l’exécution en boucle des boutons, voir ci-dessous.

Défilement des caractères

Le défilement des caractères sera géré par les touches Up et Down :

  ld a,%00100000            ;a = 0b00100000
  ldh ($00),a               ;(FF00) = a
  
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ld b,a                    ;b = a

  and %00001111             ;a &= 0b00001111
  xor %00001111             ;a ^= 0b00001111
  jr nz,notKeyDirRefresh    ;Si a != 0 : Jump notKeyDirRefresh
  xor a                     ;a = 0
  ld (KeyLastDir),a         ;(KeyLastDir) = a
  jr notKeyUp               ;Jump notKeyUp
notKeyDirRefresh:

  bit 3,b                   ;Test bit 3 de b : Down
  jr nz,notKeyDown          ;Si bit 3 != 0 : Jump notKeyDown
  ld a,(KeyLastDir)         ;a = (KeyLastDir)
  cp 4
  jr z,notKeyDown           ;Si a == 4 : Jump notKeyDown
  ld a,4                    ;a = 4
  ld (KeyLastDir),a         ;(KeyLastDir) = a
  ld a,(CharTmp)            ;a = (CharTmp)
  dec a                     ;a -= 1
  cp $20
  jr nc,processKeyDown      ;Si a >= 32 : Jump processKeyDown
  ld a,$7E                  ;a = 126
processKeyDown:
  ld (CharTmp),a            ;(CharTmp) = a
notKeyDown:

  bit 2,b                   ;Test bit 2 de b : Up
  jr nz,notKeyUp            ;Si bit 2 != 0 : Jump notKeyUp
  ld a,(KeyLastDir)         ;a = (KeyLastDir)
  cp 3
  jr z,notKeyUp             ;Si a == 3 : Jump notKeyUp
  ld a,3                    ;a = 3
  ld (KeyLastDir),a         ;(KeyLastDir) = a
  ld a,(CharTmp)            ;a = (CharTmp)
  inc a                     ;a += 1
  cp $7F
  jr c,processKeyUp         ;Si a < 127 : Jump processKeyUp
  ld a,$20                  ;a = 32
processKeyUp:
  ld (CharTmp),a            ;(CharTmp) = a
notKeyUp:

Je commence par définir puis lire le registre 0xFF00 pour les touches directionnelles (voir le chapitre précédent sur la détection des touches).

Si aucune touche est appuyée je réinitialise KeyLastDir et je saute à la fin du traitement des touches.
Sinon je vérifie les deux touches (Up et Down) tout en m’assurant que KeyLastDir n’a pas la valeur de la touche en cours (valeur définie arbitrairement par moi même).
Cette vérification a pour but d’éviter une exécution en boucle car le CPU est bien plus rapide que nos doigts, il aura le temps de passer plusieurs fois sur la détection de la touche avant qu’on ait relâché le bouton !

Puis j’incrémente ou décrémente la valeur de CharTmp tout en m’assurant de rester dans la limite des caractères que je souhaite utiliser (en l’occurrence entre 32 et 126, soit \x20 et \x7E).

Validation/Suppression du caractère

La touche A permettra de valider la sélection du caractère en cours, et la touche B supprimera le dernier caractère et permettra de le re-définir :

  ld a,%00010000            ;a = 0b00010000
  ldh ($00),a               ;(FF00) = a
  
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ldh a,($00)               ;a = (FF00)
  ld b,a                    ;b = a

  and %00001111             ;a &= 0b00001111
  xor %00001111             ;a ^= 0b00001111
  jr nz,notKeyButRefresh    ;Si a != 0 : Jump notKeyButRefresh
  xor a                     ;a = 0
  ld (KeyLastBut),a         ;(KeyLastBut) = a
  jr notKeyB                ;Jump notKeyB
notKeyButRefresh:

  bit 0,b                   ;Test bit 0 de b : A
  jr nz,notKeyA             ;Si bit 0 != 0 : Jump notKeyA
  ld a,(KeyLastBut)         ;a = (KeyLastBut)
  cp 1
  jr z,notKeyA              ;Si a == 1 : Jump notKeyA
  ld a,1                    ;a = 1
  ld (KeyLastBut),a         ;(KeyLastBut) = a
  ld a,(CharCount)          ;a = (CharCount)
  cp 20
  jr z,notKeyA              ;Si a == 20 : Jump notKeyA
  xor d                     ;d = 0
  ld e,a                    ;e = a
  ld hl,CharBuffer          ;hl = CharBuffer
  add hl,de                 ;hl += de
  inc a                     ;a += 1
  ld (CharCount),a          ;(CharCount) = a
  ld a,(CharTmp)            ;a = (CharTmp)
  ld (hl),a                 ;(hl) = a
notKeyA:

  bit 1,b                   ;Test bit 1 de b : B
  jr nz,notKeyB             ;Si bit 1 != 0 : Jump notKeyB
  ld a,(KeyLastBut)         ;a = (KeyLastBut)
  cp 2
  jr z,notKeyB              ;Si a == 2 : Jump notKeyB
  ld a,2                    ;a = 2
  ld (KeyLastBut),a         ;(KeyLastBut) = a
  ld a,(CharCount)          ;a = (CharCount)
  cp 0
  jr z,notKeyB              ;Si a == 0 : Jump notKeyB
  xor d                     ;d = 0
  ld e,a                    ;e = a
  ld hl,CharBuffer          ;hl = CharBuffer
  add hl,de                 ;hl += de
  dec a                     ;a -= 1
  ld (CharCount),a          ;(CharCount) = a
  ld a,$00                  ;a = 0
  ld (hl),a                 ;(hl) = a
notKeyB:

Le fonctionnement est très similaire aux touches Up et Down du chapitre précédent.

La touche A va ajouter CharTmp dans CharBuffer en appliquant le décalage d’adresse pour le caractère en cours (en se basant sur CharCount), puis va incrémenter CharCount afin de passer au caractère suivant.

La touche B va réinitialiser la valeur du caractère en cours dans CharBuffer puis décrémenter CharCount.

Affichage et clignotement pour le caractère en cours

Pour mettre en valeur le caractère en cours, une pratique courante est de le faire clignoter.
Pour que le clignotement soit bien synchronisé avec la fréquence d’affichage, j’ai décidé d’ajouter un compteur qui est incrémenté à chaque exécution de VBlank.

  ld a,(VBlankTimer)        ;a = (VBlankTimer)
  inc a                     ;a += 1
  ld (VBlankTimer),a        ;(VBlankTimer) = a
  ld e,a                    ;e = a

  ld hl,$9800               ;hl = $9800
  ld d,20                   ;d = 20
  ld bc,CharBuffer          ;bc = CharBuffer
displayCharBuffer:
  ld a,(bc)                 ;a = (bc)
  ldi (hl),a                ;(hl) = a, hl += 1
  inc bc                    ;bc += 1
  dec d                     ;d -= 1
  jr nz,displayCharBuffer   ;Si d != 0 : Jump displayCharBuffer

  
  ld hl,$9800               ;hl = $9800
  ld a,(CharCount)          ;a = (CharCount)
  ld b,0                    ;b = 0
  ld c,a                    ;c = a
  add hl,bc                 ;hl += bc

  xor a                     ;a = 0
  bit 4,e                   ;Test bit 4 de e
  jr z,endBlink             ;Si bit 4 == 0 : Jump endBlink
  ld a,(CharTmp)            ;a = (CharTmp)
endBlink:
  ld (hl),a                 ;(hl) = a

A chaque passage dans VBlank j’incrémente VBlankTimer puis j’actualise l’affichage du contenu de CharBuffer.

Afin d’afficher la tile au bon emplacement, j’utilise la variable CharCount pour décaler l’adresse mémoire du background au bon endroit.

Après quelques tests, le bit 4 du compteur me semble être le meilleur compris pour la vitesse de clignotement. Si il est à 0 j’affiche une tile vide, sinon j’affiche le caractère en cours.

Intégration du code SHA1

Il est temps d’intégrer l’algorithme SHA1 que j’ai réalisé dans mon précédent article : Implémentation SHA1 sur GameBoy

Un simple copier coller des variables et du code nécessaire au fonctionnement de l’algo et c’est déjà quasiment fini.

Construction du bloc SHA1

Un point que j’avais pas implémenté et qui est nécessaire ici, c’est la génération automatique du bloc SHA1.
En effet, pour fonctionner, le SHA1 fait ses opérations sur des blocs de 64 octets, mais j’ai déjà expliqué cela dans mon précédent article : Construction de la chaine à hacher (blocs SHA1)

Dans le cas présent, la saisie étant limitée à 20 caractères, le bloc SHA1 est très simple à générer :

processSha1:
  ld hl,Block               ;hl = Block
  ld bc,CharBuffer          ;bc = CharBuffer
  ld a,(CharCount)          ;a = (CharCount)
  cp 0
  jr z,loopBufferBlockEnd   ;Si a == 0 : Jump loopBufferBlockEnd
  ld e,a                    ;e = a
loopBufferBlock:
  ld a,(bc)                 ;a = (bc)
  ldi (hl),a                ;(hl) = a, hl += 1
  inc bc                    ;bc += 1
  dec e                     ;e -= 1
  jr nz,loopBufferBlock     ;Si e != 0 : Jump loopBufferBlock
loopBufferBlockEnd:
  ld a,$80                  ;a = $80
  ldi (hl),a                ;(hl) = a, hl += 1
  ld a,(CharCount)          ;a = (CharCount)
  ld e,a                    ;e = a
  ld a,62                   ;a = 62
  sub e                     ;a -= e
  ld e,a                    ;e = a
  xor a                     ;a = 0
loopInitBlock:
  ldi (hl),a                ;(hl) = a, hl += 1
  dec e                     ;e -= 1
  jr nz,loopInitBlock       ;Si e != 0 : Jump loopInitBlock
  ld a,(CharCount)          ;a = (CharCount)
  sla a                     ;a <<= 1
  sla a                     ;a <<= 1
  sla a                     ;a <<= 1
  ldi (hl),a                ;(hl) = a, hl += 1
  call sha1

  ld a,1                    ;a = 1
  ld (DisplayHash),a        ;(DisplayHash) = a

  ret

Je copie le contenu de CharBuffer dans Block en me basant sur CharCount pour savoir combien de caractères je dois copier (sachant qu’il peut aussi y avoir 0 caractères).

Puis je fini la construction du bloc avec le caractère \x80 suivi du remplissage des null bytes et enfin le dernier caractère représentant la longueur de la chaine.

Après avoir appelé le calcul du sha1 je définit ma variable DisplayHash à 1 afin de déclencher l’affichage lors du prochain VBlank.

Affichage du hash

On peut désormais passer à l’affichage du hash SHA1.
Puisqu’on doit manipuler la mémoire vidéo il faut faire cela dans le VBlank, ce qui implique de prendre en compte la limitation de 4560 cycles comme expliqué dans le chapitre Interruption V-Blank.

  ld a,(DisplayHash)        ;a = (DisplayHash)
  cp 1
  jr nz,tryDisplayHash      ;Si a != 1 : Jump tryDisplayHash
  inc a                     ;a += 1
  ld (DisplayHash),a        ;(DisplayHash) = a

  ld hl,$9860               ;hl = $9860
  ld d,20                   ;d = 20
  xor a                     ;a = 0
loopCleanText:
  ldi (hl),a                ;(hl) = a
  dec d                     ;d -= 1
  jr nz,loopCleanText

  ld hl,$9860               ;hl = $9860
  ld bc,CharBuffer          ;bc = CharBuffer
  ld a,(CharCount)          ;a = (CharCount)
  cp 0
  jr z,displayHashEnd       ;Si a == 0 : Jump displayHashEnd
  ld d,a                    ;d = a
loopDisplayText:
  ld a,(bc)                 ;a = (bc)
  ldi (hl),a                ;(hl) = a, hl += 1
  inc bc                    ;bc += 1
  dec d                     ;d -= 1
  jr nz,loopDisplayText     ;Si d != 0 : Jump loopDisplayText
  jr displayHashEnd         ;Jump displayHashEnd

tryDisplayHash:
  cp 2
  jr nz,displayHashEnd      ;Si a != 2 : Jump displayHashEnd
  ld hl,$98C0               ;hl = $98C0
  ld bc,StateA              ;bc = StateA
  ld d,4                    ;d = 4
  ld e,5                    ;e = 5
  jr loopDisplayHash        ;Jump loopDisplayHash
loopDisplayHash2:
  push bc                   ;Stack bc
  ld d,4                    ;d = 4
  ld bc,24                  ;bc = 24
  add hl,bc                 ;hl += bc
  pop bc                    ;UnStack bc
loopDisplayHash:
  ld a,(bc)                 ;a = (bc)
  srl a                     ;a >>= 1
  srl a                     ;a >>= 1
  srl a                     ;a >>= 1
  srl a                     ;a >>= 1
  cp 10
  jr c,displayHashHex1      ;Si a < 10 : Jump displayHashHex1
  add 7                     ;a += 7
displayHashHex1:
  add $30                   ;a += 48
  ldi (hl),a                ;(hl) = a, hl += 1
  ld a,(bc)                 ;a = (bc)
  and $0F                   ;a &= 15
  cp 10
  jr c,displayHashHex2      ;Si a < 10 : Jump displayHashHex2
  add 7                     ;a += 7
displayHashHex2:
  add $30                   ;a += 48
  ldi (hl),a                ;(hl) = a, hl += 1
  inc bc                    ;bc += 1
  dec d                     ;d -= 1
  jr nz,loopDisplayHash     ;Si d != 0 : Jump loopDisplayHash
  dec e                     ;e -= 1
  jr nz,loopDisplayHash2    ;Si e != 0 : Jump loopDisplayHash2

  xor a                     ;a = 0
  ld (DisplayHash),a        ;(DisplayHash) = a
  jr endVBlank              ;Jump endVBlank
displayHashEnd:

Afin de séparer l’affichage en plusieurs parties (pour ne pas dépasser le nombre de cycles) j’utilise la variable DisplayHash pour savoir quelle partie je dois afficher.
Si mes calculs sont bons, je dépasse jamais la limite des 4560 cycles !

Si DisplayHash est égal à 1 j’incrémente sa valeur (pour le prochain passage dans VBlank) puis je génère l’affichage du texte en commençant par remettre toutes les tiles de la ligne à 0 puis en affichant le contenu de CharBuffer.

Si DisplayHash est égal à 2 alors je procède à l’affichage du hash sur 5 lignes de 4 * 2 caractères (pour une représentation hexadécimale).
Je parcours donc les octets en commençant par StateA, puis tous les 4 octets je passe à la ligne suivante (les 5 variables State se suivent dans la mémoire, il suffit donc juste d’incrémenter l’adresse depuis le point de départ StateA).

Pour avoir l’affichage en hexadécimal, je récupère dans un premier temps les 4 bits de poids fort (via 4 décalages vers la droite).
Si la valeur est supérieure à 9 c’est qu’il s’agit d’une lettre auquel cas j’ajoute 7 en plus de 48, ainsi je fais correspondre avec le bon caractère (le décalage de 7 vient des 7 caractères entre les chiffres et les lettres dans la table ASCII).

Le principe est identique pour les 4 bits de poids faible, je les récupère avec un and 0b00001111 puis je fais exactement les mêmes opérations que pour les 4 bits précédents.

Pour finir, je repasse DisplayHash à 0 afin de permettre de relancer un nouveau calcul SHA1 et donc un nouvel affichage.

Conclusion

Au début j’avais un doute sur la nécessité de faire un article sur ce sujet.
Puis quand j’ai commencé à me renseigner et à faire mes tests, j’ai trouvé ça vraiment très intéressant et il aurait été dommage de ne pas partager tout ça !

Même si ça semble très simple, il y a assez de possibilités et de subtilités pour justifier la rédaction d’un article.

Comme d’habitude, il y a encore pas mal d’optimisations possibles et je suis preneur de vos propositions !

Et encore une fois, un grand merci à entropyQueen pour l’idée du projet et pour le test de mon code en conditions réelles sur sa GameBoy !
(Hé oui, je n’ai pas encore le matériel pour tester sur ma GB, je me contente de l’émulateur)

Sources et inspirations

  • Code du projet
  • Polices d’écriture GB
  • Mon premier article
  • Spécifications GameBoy
  • Blog d’entropyQueen
  • Article de Furrtek
  • Suite d’articles de FLOZz