codefordumm!ies: the cool logo

J'apprends la programmation avec l'assembleur x86, partie 8

Dans cette partie 8, nous allons allumer des pixels dans le mode VGA 320x200 et afficher non pas du fanta ou du coca, mais du sprite !

piou! piou!

Vive le président Point Carré !

Nous avons vu dans le chapitre 6 que notre écran VGA était composé de 200 lignes de 320 pixels chacunes et que chaque pixel pouvait avoir une couleur parmi, soit une valeur entre 0 et 255.
Pour afficher des images sur cette écran, il va falloir mettre la bonne valeur de couleur au bon endroit. Pour obtenir cette incroyable résultat, on va écrire la fonction put_pixel.
Nous allons passer 3 paramètres sous forme de variables à notre fonction :

L'oeuf, c'est ?

Pour trouver la position d'un pixel par rapport à l'écran, on va devoir faire un peu de maths :

Ce calcul va nous donner l'offset du pixel par rapport à l'adresse de 0xa000:0x0000 de la carte VGA, c'est à dire le décallage par rapport au début de cette mémoire.
Bien évidemment (comme le dirait tout prof de math qui se respecte en regardant les yeux hagards et plein de désespoir de ses élèves...) pour que cela fonctionne, le premier pixel de l'écran doit avoir pour coordonnées 0 ; 0 et non 1 ; 1 !
Et oui, avec ce calcul, le pixel 1 ; 1 est à l'offset 1 × 320 + 1 soit 321, et donc à la colonne 2 de la deuxième ligne de notre écran.

Les plus perspicaces d'entre vous en auront déduit que le tout dernier pixel de notre écran a pour coordonnées : 319 ; 199.

Une vraie tête de mul

Et voici comment mettre ce calcul en place, en utilisant l'instruction mul.
mul permet de multiplier les valeurs de ax et dx et le résultat sera stocké dans le même couple de registre dx:ax.
Bin oui, si on multiplie une valeur de 16 bits par une autre valeur de 16 bits, le résultat peut faire jusqu'à 32 bits, une vraie orgie ! Dans ax sera stockés les valeurs entre 0 et 65535, et dans bx les valeurs supérieurs à 65535 :

  mov ax, VGA_WIDTH     ; ax = 320
  mov dx, [y]           ; dx = y
  mul dx                ; dx:ax = 320 * y   

Et comme 320 × 200 ne dépassera jamais 64000, on utilisera juste ax pour le résultat (mais dx sera modifié dans tous les cas par mul).

Enfin, une fois notre multiplication effectuée, on additionne x au résultat, on stocke ça dans di et on peut utiliser stosb comme on l'a vu dans les chapitres précédents pour pouvoir placer notre pixel en mémoire.

  mov di, ax            ; di = 320 * y
  add di, [x]           ; di = 320 * y + x
  mov al, [clr]         ; al = clr
  stosb                 ; [es:di] = clr

Bon on va pas se mentir, cette version de put_pixel n'est pas la plus optimisée du monde, mais elle fait ce qu'on lui demande, ce qui est déjà pas si mal.

L'ennui des Masques...

Maintenant que l'on sait comment afficher un pixel où bon nous semble, on peut s'attaquer au dessin de nos deux sprites.

Pour notre exemple, nos sprites sont déclarés sous la forme de 8 word où chaque bit représente un pixel allumé (valeur 1) ou un pixel éteint (valeur 0). L'idée est donc de parcourir chacun des bits de ces word, et d'afficher un pixel si ce bit est à 1.
Chaque word correspond à une ligne du sprite.

C'est selon ce même principe que les caractères sont stockés dans la mémoire ROM du PC, mais avec plus ou moins de bytes suivant leur taille.

Pour le premier sprite par exemple :

sprite1: dw 0x0820, 0x0440, 0x0fe0, 0x1bb0, 0x3ff8, 0x2fe8, 0x2828, 0x06c0

Ce qui nous donnera :

0000100000100000    0x0820
0000010001000000    0x0440
0000111111100000    0x0fe0
0001101110110000    0x1bb0
0011111111111000    0x3ff8
0010111111101000    0x2fe8
0010100000101000    0x2828
0000011011000000    0x06c0

Mais bon, on voit pas grand chose... Remplaçons les 0 par des espaces :

    1     1         0x0820 
     1   1          0x0440 
    1111111         0x0fe0 
   11 111 11        0x1bb0 
  11111111111       0x3ff8
  1 1111111 1       0x2fe8
  1 1     1 1       0x2828
     11 11          0x06c0

T'es pas une shift molle !

Dans la fonction draw_sprite on va dans un premier temps lire le word correspondant à la ligne du sprite :

draw_sprite_line:
  lodsw                 ; ax = [ds:si] = ligne du sprite
  mov dx, [x]           ; dx = x
  mov bx, 0x8000        ; notre masque
  mov ch, 16            ; compteur bit : 16 bits par lignes

Puis on va utiliser le masque 0x8000 (soit en binaire 1000000000000000) pour tester si le premier bit de gauche de notre word est à 1 :

draw_sprite_bit:
  test bx, ax           ; test bit & masque
  jz draw_sprite_no_pixel   ; si 0 on ne dessine rien, sinon...
  call put_pixel        ; ... on dessine le pixel

test est l'instruction qui teste (c'est bien trouvé comme nom...) si il y a au moins 1 bit en commun entre les deux registres. On l'a déjà vu dans le chapitre précédent. On peut faire un jump en fonction de l'état de ce flag avec :

Si c'est le cas, on appelle put_pixel pour afficher le pixel, puis on passe au pixel suivant :
x est augmenté de 1 et on fait un shift/décallage du masque vers la droite de 1 bit avec shr :

draw_sprite_no_pixel:
  shr bx, 1             ; masque pour le bit suivant
  inc dx                ; x = x + 1

bx va passer de la valeur en binaire 1000000000000000 à 0100000000000000.
On pourra maintenant tester le second bit le plus à gauche et ainsi de suite pour les 16 bits d'une ligne du sprite.

Puis on répète le processus pour les lignes suivantes.

Les sprites, montres-toi !

On n'est pas obligé d'utiliser ce système de masque pour afficher des sprites, mais c'est un sympathique exemple de son usage.

Le code source de invaders.asm

; ========
; invaders
; ========
;
; C:\nasm -fbin invaders.asm -o invaders.com    

org 0x100

global main

VGA_MEMORY_SEGMENT  equ 0xa000  ; segment mémoire de la carte VGA
VGA_WIDTH       equ 320     ; nombre de pixels horizontaux en VGA
VGA_HEIGHT      equ 200     ; nombre de pixels verticaux en VGA

section .text

main:
  call enter_vga    ; on entre dans le mode VGA
  call render       ; on dessine un truc
  call wait_key     ; on attend une touche au clavier
  call quit_vga     ; on quitte le mode VGA

  mov ax, 0x4c00    ; on quitte sans erreur
  int 0x21

enter_vga:
  mov ah, 0x0f      ; on récupère le mode vidéo en cours
  int 0x10      ; dans al
  mov [old_mode], al    ; on conserve cette valeur dans old_mode

  mov ah, 0x00      ; on change le mode vidéo
  mov al, 0x13      ; AL = 0x13 soit VGA 320x200x256 couleurs
  int 0x10
  ret

quit_vga:
  mov ah, 0x00      ; on restaure le mode vidéo 
  mov al, [old_mode]    ; conservé dans old_mode
  int 0x10
  ret

wait_key:
  mov ah, 0x01      ; on vérifie si une touche a été appuyée
  int 0x16
  jz wait_key       ; si ZF est activé, aucune touche n'a été appuyée
  ret

render:
  mov ax, VGA_MEMORY_SEGMENT
  mov es, ax

  xor dx, dx            ; dx est l'index du sprite, soit 0
  mov [sprite_id], dx
  mov ax, 146           ; x = 146
  mov [x], ax
  mov ax, 92            ; y = 92
  mov [y], ax
  mov al, 4             ; clr = rouge
  mov [clr], al
  call draw_sprite      ; on dessine le premier sprite

  inc dx                ; dx est l'index du second sprite
  mov [sprite_id], dx
  mov ax, [x]           ; x = x + 16
  add ax, 16
  mov [x], ax
  mov al, 2             ; clr vert
  mov [clr], al
  call draw_sprite      ; on dessine le second sprite

  ret

put_pixel:
  push ax
  push dx
  push di

  mov ax, VGA_WIDTH     ; ax = 320
  mov dx, [y]           ; dx = y
  mul dx        ; dx:ax = 320 * y
  mov di, ax            ; di = 320 * y
  add di, [x]           ; di = 320 * y + x
  mov al, [clr]         ; al = clr
  stosb                 ; [es:di] = clr

  pop di
  pop dx
  pop ax
  ret

draw_sprite:
  pusha                 ; on sauve tous les registres courants

  mov ax, [y]           ; on sauve y
  push ax
  mov ax, [x]           ; on sauve x
  push ax
  mov [prevx], ax         

  mov ax, [sprite_id]   ; ax = sprite_id
  shl ax, 4             ; ax = sprite_id * 16: il y a 16 bytes par sprites
  mov si, sprite1       ; si est l'offset du premier sprite
  add si, ax            ; on ajoute à si l'offset du sprite choisi par sprite_id

  mov cl, 8             ; compteur ligne : 8 lignes par sprites
draw_sprite_line:
  lodsw                 ; ax = [ds:si] = ligne du sprite
  mov dx, [x]           ; dx = x
  mov bx, 0x8000        ; notre masque
  mov ch, 16            ; compteur bit : 16 bits par lignes
draw_sprite_bit:
  test bx, ax           ; test bit & masque
  jz draw_sprite_no_pixel   ; si 0 on ne dessine rien, sinon...
  call put_pixel        ; ... on dessine le pixel
draw_sprite_no_pixel:
  shr bx, 1             ; masque pour le bit suivant
  inc dx                ; x = x + 1
  mov [x], dx               
  dec ch                ; on passe au  bit suivant  
  jnz draw_sprite_bit

  push ax               ; on conserve ax qui va nous servir pour affecter les variables
  mov ax, [y]           ; y = y + 1
  inc ax
  mov [y], ax               
  mov ax, [prevx]       ; on recupère la valeur de x
  mov [x], ax
  pop ax                ; on récupère ax
  dec cl                ; on passe à la ligne suivante
  jnz draw_sprite_line

  pop ax                ; on restaure x
  mov [x], ax
  pop ax                ; on restaure y
  mov [y], ax         

  popa                  ; on restaure tous les registres courants
  ret

section .data
old_mode: db 0          ; variable pour conserver le mode vidéo précédent
x: dw 0                 ; x du pixel    
prevx: dw 0             ; prevx pour conserver x dans notre boucle
y: dw 0                 ; y du pixel
clr: db 0               ; couleur du pixel
sprite_id: dw 0         ; index du sprite (0 ou 1)  
; ci dessous les lignes des deux sprites :
sprite1: dw 0x0820, 0x0440, 0x0fe0, 0x1bb0, 0x3ff8, 0x2fe8, 0x2828, 0x06c0
sprite2: dw 0x0300, 0x0780, 0x0fc0, 0x1b60, 0x1fe0, 0x0480, 0x0b40, 0x14a0

Pour m'entrainer

Essayez d'afficher le négatif de ces sprites comme illustré magnifiquement ci-dessous :

piou! piou!i mais en négatif

Solution