Suite SHA1 sur GameBoy : Interface graphique
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 :
- On définit les bits 4 et 5 dans le registre
0xFF00
afin de spécifier quelles touches on souhaite lire - 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)