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 !

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 :
- x : un word pour la position horizontale
- y : un autre word pour la position verticale
- clr : un byte pour la couleur On pourrait utiliser uniquement un byte pour y comme la valeur maximale de celui-ci est de 200, mais c'est plus simple ainsi.
L'oeuf, c'est ?
Pour trouver la position d'un pixel par rapport à l'écran, on va devoir faire un peu de maths :
- on cherche d'abord la ligne de l'écran en multipliant y par le nombre de pixels d'une ligne soit :
y × 320 - puis on y ajoute la position x :
y × 320 + x
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 :
- jz jump si le flag est activé
- jnz jump si le flag n'est pas activé
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 :
