Triceraprog
La programmation depuis le Crétacé

VG5000µ, SetPoint en ASM, afficher le point ()

À présent que l'on sait diviser par 3, reprenons l'affichage d'un point à l'écran. Pour rappel.

En entrée, nous avons : des coordonnées X et Y, comprises entre 0 et 79 pour X et 0 et 74 pour Y.

En effet de bord, c'est-à-dire en modification de l'état de la machine, nous voulons : le point correspondant à l'écran qui prend la couleur d'encre définie.

Pour cette version, la procédure ne prendra pas d'information de couleur, je me contenterai d'utiliser la couleur d'encre 0 (noir) sur fond 6 (bleu), qui est la combinaison à l'initialisation de la machine.

Les étapes, d'après les articles précédents, sont donc :

  • À partir de X et Y, trouver les coordonnées du caractère à modifier à l'écran
  • À partir de X et Y, trouver les coordonnées à l’intérieur du caractère semi-graphique
  • À partir de coordonnées du caractère, calculer l'adresse mémoire écran correspondante
  • Récupérer les valeurs pour la paire d'adresse mémoire
  • Si le caractère présent n'était pas un caractère semi-graphique standard, considérer qu'il était complètement éteint (valeur 0 pour le caractère)
  • Modifier la valeur du caractère récupéré en fonction des coordonnées à l'intérieur du caractère semi-graphique
  • Modifier la mémoire écran avec les nouvelles valeurs

Comme le code est plutôt long, je vais changer de méthode. Le code entier va suivre, commenté au maximum en ligne.

Le Code

        ; La procédure se nomme 'setpoint' et sera appelée avec `call setpoint`
        ;
        ; Les coordonnées (x,y) du point à allumer sont mises dans, respectivement
        ; L et H. Autrement dit, HL contient yx.
        ;
        ; C'est le format utilisé par la ROM du VG5000µ pour ses coordonnées de
        ; curseur. Autant le garder.
        ;
setpoint:
        ; La procédure sauve tous les registres exceptés IX et IY.
        ;
        ; On pourrait aussi considérer que c'est à l'appelant de veiller à
        ; garder ses registres intègres
        ;
        ; Ce n'est pas le plus efficace, mais pour le moment, c'est le plus sûr.
        push    hl
        push    bc
        push    af
        push    de

        ; Pour le moment :
        ; - HL contient les coordonnées (y,x)
        ; - Les autres registres sont libres

        ld      a,h             ; On travaille sur y
        call    div3            ; Que l'on divise par 3
        ld      d,a             ; Et donc D contient y/3

        ld      a,l             ; On travaille sur x

        ; La ligne suivante ne fonctionne que si C (le Drapeau de retenue)
        ; est bien à zéro (ce qui est assuré avec le div_3 utilisé)
        ; Sinon, il faudrait utiliser `srl a`, qui est codé sur deux octets plutôt que 1
        rra                     ; On divise A par 2
        ld      e,a             ; Et donc D contient x/2

        ; À ce point :
        ; - DE contient les coordonnées (y/3,x/2)
        ; - HL contient toujours les coordonnées (y,x)
        ; - Les autres registres sont libres

        ; B sera utilisé temporairement
        ld      a,l             ; On travaille à nouveau sur x
        and     $01             ; On ne garde de A que le bit de poids faible.
        ld      b,a             ; Et donc B contient x modulo 2 (le reste de la division entière par 2)

        ld      a,h             ; On travaille à nouveau sur y
        sub     d               ; On soustrait D, qui contient y/3 (partie entière)
        sub     d               ; Une seconde fois
        sub     d               ; Puis une troisième fois.
                                ; Et donc A contient y modulo 3

        ; On remarque ici qu'une fonction qui retournerait en même temps le quotient
        ; ET le reste de la division entière pourrait faire gagner un peu de temps...

                                ; On travaille dessus immédiatement sur (y modulo 3)
                                ; Dorénavant, j'utiliserai le signe % pour modulo.

        add     a               ; A contient à présent (y % 3) * 2
        add     b               ; A contient à présent (y % 3) * 2 + (x % 2)

        ; Petit point :
        ; - A contient la puissance de 2 nécessaire à trouver le bon caractère
        ; - DE contient toujours les coordonnées (y/3,x/2)
        ; - BC ne contient plus rien d'intéressant
        ; - HL ne contient plus rien d'intéressant
        ;   À vrai dire, HL n'est plus utile depuis le ld a,h précédent

        or      a               ; Équivalent à cp $0 mais plus conci et rapide
                                ; Le résultat de cette comparaison de A avec 0
                                ; va être conservé par les drapeaux jusqu'au
                                ; JR suivant, car les instructions LD n'altèrent
                                ; par les drapeaux.

        ; Il faut à présent calculer la valeur 2 à la puissance A

        ld      b,a             ; On charge B avec la valeur A. B va servir de
                                ; compteur de boucle.
        ld      a,1             ; On initialise le résultat à 1

        jr      z,no_power      ; Si A était égal à zéro, on n'a rien besoin
                                ; de calculer, donc on passe à la suite.

        ; La boucle suivante décale vers la gauche le contenu de A de 1 position
        ; Autrement dit, A est multiplié par 2 à chaque tour de boucle.
        ; À la fin de la boucle, A contient donc 2 puissance B.
power_of_2:
        rla                     ;
        djnz    power_of_2

no_power:
        ; A contient l'index du caractère semi-graphique à aller chercher.
        ; On sauve cette valeur pour plus tard dans la pile.
        push af

        ; Petit point :
        ; - HL est libre
        ; - BC est libre
        ; - DE contient les coordonnées (y/3, x/2)
        ; - A est sauvé sur la pile, on pourra donc l'utiliser pour des calculs

        ; L'objectif est à présent d'aller calculer l'adresse mémoire du caractère
        ; à changer dans la plage mémoire dédiée en RAM.

        ; La fonction de multiplication que j'utilise ici, et qui n'aura pas
        ; son article dédié, utilise HL et DE. DE est transféré dans BC.
        ld      b,d
        ld      c,e

        ; Et donc à présent DE est libre
        ; Et BC contient (y/3, x/2)

        ; Le premier calcul à faire est (y/3)*80
        ld      h,80            ; H contient 80
        ld      e,b             ; E contient y / 3

        call    mult            ; Appel de la multiplication
                                ; À présent, HL contient (y/3)*80

        ; Le second calcul à faire est d'arrondir X à l'entier pair
        ; inférieur le plus proche. Pour cela, (x/2)*2, en utilisant
        ; une division entière donne le ŕésultat.

        ld      a,c             ; A contient x/2 (division entière)
        add     a               ; A contient (x/2)*2 (division entière)

        ; Petit point :
        ; - HL contient le déplacement mémoire sur le début de la ligne
        ; - A contient le déplacement en colonnes sur la ligne
        ; - BC est libre
        ; - DE est libre

        ; Il faut donc additionner HL et A pour avoir l'index mémoire.
        ; Le Z80 ne peut pas faire ça directement. Il faut donc charger
        ; A dans un registre 16 bits. BC par exemple.
        ld      b,0
        ld      c,a

        add     hl,bc           ; HL contient à présent (x/2)*2 + (y/3)*80

        ; On se ressert de BC pour indiquer la base de l'adresse mémoire vidéo
        ; auquel on ajoute l'index, pour obtenir l'adresse mémoire dans HL.
        ld      bc,$4000
        add     hl,bc

        ; Il est temps d'aller chercher les informations déjà présentes
        ; en mémoire. Pour cela, on a besoin des deux adresses HL et HL+1
        ; (voir les articles sur l'agencement de la mémoire vidéo)
        ld      b,h
        ld      c,l

        inc     bc              ; BC contient HL + 1

        ld      a,(bc)          ; A contient donc la valeur d'attribut du caractère
        bit     7,a             ; Ce qui nous intéresse est sont bit numéro 7
        jr      z,set_base_char ; S'il est à 0, ce n'est pas un caractère semi-graphique

                                ; Sinon, on a besoin de connaître la valeur actuelle de ce
                                ; caractère

        ld      a,(hl)          ; On récupère la valeur du caractère semi-graphique
                                ; actuellement à l'écran dans A

        ; Si le caractère fait partie de la place 64 à 127 (les caractères pleins)
        ; alors on continue plus loin.
        bit     7,a
        jr      z,char_ok

set_base_char:
        ld      a,64            ; Dans le cas où le caractère à l'écran n'était pas
                                ; semi-grapique plein, on part sur une base du
                                ; caractère 64, qui est le caractère semi-graphique
                                ; 'tout éteint'
char_ok:
        pop     de              ; Recupération dans D de l'index du caractère calculé.
                                ; Cette valeur vient du 'push af' effectué plus haut.

        or      d               ; Une opération bit à bit 'OU' entre l'ancienne valeur
                                ; et le nouvel index donne le nouveau caractère.

        ld      (hl),a          ; Ce caractère est placé à l'écran

        ld      a,224           ; Et dans cette implémentation, on fixe les attributs
        ld      (bc),a          ; selon les valeurs de couleurs à l'allumage du VG5000µ
                                ; Une amélioration sera d'aller chercher dans les variables
                                ; systèmes quels sont les couleurs courantes.

        ; La routine se termine, on restitue la valeur de tous les registres
        ; utilisés pour revenir à l'appelant.
        pop de
        pop af
        pop bc
        pop hl


        ret

div3:                           ; Entrée: registre A, Sortie: valeur divisée par 3, dans A
        exx                     ;
        ld      hl,div3_table
        ld      b,0
        ld      c,a
        add     hl,bc
        ld      a,(hl)

        exx                     ;
        ret

div3_table:
        defb    0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5 ; 18
        defb    6,6,6,7,7,7,8,8,8,9,9,9,10,10,10,11,11,11
        defb    12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17
        defb    18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23

mult:                           ; Entrée, registre H et registre E
                                ; Sortie, le registre HL comtient le résultat
                                ; de l'opération H * E
                                ; Utilise HL, B, DE
        ld      d,0
        ld      l,d
        ld      b,8
mult_loop:
        add     hl,hl
        jr      nc,mult_skip
        add     hl,de
mult_skip:
        djnz    mult_loop
        ret

Comment est-ce que ça s'utilise ?

Cette routine s'utilise donc en mettant dans HL les coordonnées (y, x) du point à afficher. Elle pourrait être améliorée en déterminant si on veut afficher ou éteindre le point, spécifier les couleurs ou les récupérer des variables systèmes. Il y a probablement quelque optimisations qui trainent.

Toujours est-il que par rapport à la routine est basique, l'exécution sera beaucoup plus rapide. Il serait possible d'être encore plus réactif en s'adressant directement au processeur vidéo. Mais je préférais utiliser le buffer vidéo en RAM pour plus de simplicité. Une implémetnation utilisant le processeur vidéo peut se trouver dans la bibliothèque d'affichage de Z88DK pour le VG5000µ. Z88DK est tout un système, basé sur un compilateur C, pour programmer les machines Z80.

Pour revenir à l'utilisation de cette routine, voici un exemple qui affiche plusieurs lignes horizontales à l'écran.

        org     $7000

        ; Sauvegarde des registres utilisés.
        push    hl
        push    bc

        ld      h,$0A           ; La coordonnée y est 10 ($A en hexa)
        ld      b,25            ; On prépare une boucle de 25 itérations
loop_1:
        ld      c,b             ; Sauvegarde temporaire de la boucle externe
                                ; afin de préparer une boucle interne

        ld      l,$10           ; La coordonnée x est 16 ($10 en hexa)
        ld      b,40            ; On prépare une boucle de 40 itérations
loop_2:
        call    setpoint        ; On affiche un point
        inc     l               ; On incrémente la coordonnée x de 1
        djnz    loop_2          ; Et on boucle
                                ; Ce qui affiche une ligne de 40 pixels de large
                                ; à la coordonnée y courante.

        ld      b,c             ; Récupération de l'index de boucle externe

        inc     h               ; On incrémente la coordonnée y deux fois
        inc     h               ; on "saute" donc une ligne.

        djnz    loop_1          ; Et on recommence ceci 25 fois.
                                ; On affiche donc 25 lignes les unes sous les autres
                                ; séparées à chaque fois par une hauteur d'un pixel

        ; Restauration des registres utilisés et retour à l'appelant
        pop     bc
        pop     hl

        ret

Résultat

Affichage des résultats de tests dans MAME