II. VESA en mode protégé▲
Relu par ClaudeLELOUP.
Oui, chers amis, vous avez bien lu : enfin de vrais graphismes en mode protégé ! Adieu mode VGA, SVGA, Hercules et autres reliquats des années 1990 !
II-1. Des émulateurs▲
Si vous m'avez suivi jusqu'ici, vous devez savoir que nous travaillons sur émulateur. Si jusqu'à présent, n'importe quel émulateur pouvait vaguement convenir à quelques exceptions près, ce n'est plus le cas. Warum ? Weil komssa ist. Traduction : pourquoi ? Parce que visiblement maints émulateurs ont bien des difficultés à reproduire correctement le VESA 2.0. Oui, je sais, l'allemand est une langue magnifique.
Alors, bon, voici les émulateurs que j'ai testés, si vous avez d'autres retours, dites-le-moi.
- Bochs : je ne me permettrai pas de critiquer vu que plein de gens en sont très contents, mais je n'ai même pas réussi à avoir un vague VESA 1.2.
- VMWare : le VESA 2.0 est spécifiquement écrit dans l'OEM, mais soit je suis nul, soit c'est une mauvaise blague. Sur un paquet de 80 ko (kilo-octet), les derniers 40 ko pointent sur les mêmes pixels que les premiers 40 ko, ce qui donne des fascés.
- VirtualBox : ça marche. Enfin, à peu près. 'Faut apparemment pas être trop rapide, notamment pour un remplissage standard d'écran. Sinon, leur VESA 2.0 a un comportement de VESA 2.0.
Donc la suite se déroulera sur VirtualBox.
II-2. Du Linear Frame Buffer▲
Qu'on sigle LFB. C'est un champ donné à l'octet 40 dans la structure renvoyée par l'appel BIOS 0x10, fonction 0x4F01, quand on demande des informations sur un mode vidéo particulier. Il s'agit d'une adresse sur 32 bits, à partir de laquelle on trouvera la valeur de chaque pixel de l'écran.
Et vous savez quoi ? Pour éviter des additions fastidieuses et nous accélérer le tempo, nous allons déclarer un segment de données qui commence à cette adresse. Après tout, on a appris à jouer avec la GDT, oui ou non ?
gdt:
db 0x00, 0x00, 0x00, 0x00, 0x00, 00000000b, 00000000b, 0x00
gdt_cs:
db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10011011b, 11011111b, 0x00
gdt_ds:
db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10010011b, 11011111b, 0x00
gdt_flb:
dw 0xFFFF, 0x0000
db 0x00, 10010011b, 11011111b, 0xE0
gdtend:
pointeurGDT:
dw gdtend-gdt ; taille
dd (BOOT_SEG << 4) + gdt ; baseC'est initialisé à 0x00E00000, je crois. C'est parce que VirtualBox place son écran à cet endroit-là . De toute façon, on peut mettre n'importe quoi, ce sera initialisé proprement à la lecture. On ne met pas de limite supérieure au segment, parce que je n'en ai ni envie ni besoin pour l'instant. Le mélange rock'n roll de mots et d'octets n'a pour objectif que de me faciliter la création à la main.
On remplit comme ça :
mov ax, [FlatMemory]; bits de poids faible
mov [gdt_flb + 2], ax
mov ax, [FlatMemory + 2]; bits de poids fort
mov [gdt_flb + 4], al
mov [gdt_flb + 7], ahOn fait un usage intensif de la structure d'un descripteur de segment, donnée au chapitre 8. Ah, oui, c'est vrai : je fais ici, dans le chargeur d'amorçage, référence à FlatMemory, mémoire aplatie. Cette valeur est une adresse codée en dur par un %define :
;Définitions de données qui seront chargées par le BIOS
;Informations du pilote VESA
%define VESASignature 512;'VESA', signature de 4 octets
%define VideoModePtr VESASignature + 14;: dd 0; Pointeur vers les modes accessibles
;Informations d'un mode vidéo
%define ModeAttributes VESASignature + 256; Attributs du mode mot
%define BytesPerScanLine ModeAttributes + 16;: dw 0; Octets par "scanline" mot
%define XResolution ModeAttributes + 18;: dw 0; Résolution horizontale
%define YResolution ModeAttributes + 20;: dw 0; Résolution verticale
%define BitsPerPixel ModeAttributes + 25;: db 0; Bits par pixel
%define FlatMemory ModeAttributes + 40Parce que, si on ne mettait pas cette adresse en dur, l'ensemble de ces deux jeux de données ferait 512 octets, soit précisément la taille de notre secteur d'amorçage, et nous n'aurions donc pas de place pour du code. Plutôt que de faire des chargements de secteur, de vérifier qu'on a bien ce qu'on veut là où on le veut, je sais qu'à la fin du segment (on est en 16 bits !), j'ai de l'espace, vu que je n'ai dedans que 512 octets, je balance ça à une adresse fixée à cet endroit.
L'adresse du LFB oblige à se poser la question suivante : que deviennent les deux autres fenêtres, les célèbres fenêtres A et B qu'on utilise en mode réel ? Elles existent toujours et sont définies, mais à première vue, leur utilisation est incompatible avec celle du LFB. Et de fait, les deux utilisations sont incompatibles. Pour utiliser le LFB, il faut, lors de l'appel au changement de mode, passer le bit n° 14 à 1, chose que je fais ainsi :
mov ax, 0x4F02
mov bx, [mode_souhaite]
or bx, 0100000000000000b ; Le bit n°14 à 1 demande l'utilisation de la mémoire linéaire.
int 0x10 ; Changement de mode vidéoDétail pendant que j'y pense : le binaire peut aussi se suffixer par 'b'.
II-3. Du dessin▲
Allez, jeune France, on y retourne ! Je ne donne pas la fonction de nettoyage d'écran vu qu'elle ne fonctionne pas bien, mais si quelqu'un se sent l'âme d'un peintre en bâtiment, je suis preneur. Comme on n'a pas d'écran de 65536 par 65536, on peut utiliser un registre de 32 bits pour coder à la fois l'abscisse et l'ordonnée. L'abscisse sera ici le mot de poids fort et l'ordonnée le mot de poids faible. On met EDI à 0, le sélecteur de segment ES sur le segment n° 3 (vérifiez la table) avant l'appel, deux multiplications et une somme, on écrit et c'est fini.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; metPixel ;;
;;------------------------------------------------------------------;;
;; Colorie le pixel dont la position est dans EAX (mot haut : X, ;;
;; mot bas : Y) avec la couleur contenue dans couleurPoint ;;
;; ES doit contenir le descripteur de segment de l'écran. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
metPixel:
push edi
push ecx
push eax
push ebx
push edx
mov ebx, eax
and eax, 0xFFFF
shr ebx, 16
xor ecx, ecx
mov cx, [XResolution]
mul ecx ; On fait une multiplication 32 bits à partir de valeurs de 16 bits.
add eax, ebx
xor ch, ch
mov cl, [octetsParPixel]
mul ecx ; On fait une multiplication 32 bits à partir de valeurs de 8 bits.
mov edi, eax
mov eax, [couleurPoint]
stosd
.retour:
pop edx
pop ebx
pop eax
pop ecx
pop edi
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin metPixel ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;J'espère que vous avez vu l'imbécillité que j'ai faite, je la laisse pour l'édification des masses.
Allez, entre les push et les pop, y'a pas beaucoup de code, mes enfants !
Mettre la couleur, c'est obligé. Ce n'est pas ça.
Calculer la coordonnée, obligatoire aussi.
Il ne reste que la bourde.
Oui, le nombre d'octets par pixel. On écrit 32 bits de couleur, le nombre d'octets par pixel est donc nécessairement de 4. Ce qui implique que dans le code d'amorçage, on ne sélectionne que les modes à 32 bits. Autre subtilité : comme on fait une multiplication, le registre EDX est modifié. Il faut donc le mettre sur la pile au début de la fonction et le restaurer à la fin. Pensez-y, on peut perdre des heures bêtement.
II-4. De Bresenham▲
Je vous en avais déjà parlé il y a bien longtemps, voici le retour de l'algorithme de Bresenham, l'étoile des dessinateurs de segment de droite. Sauf qu'à l'époque, il s'agissait d'une interprétation quelque peu cavalière. Nous allons faire mieux, de surcroît en plus court.
II-4-a. L'idée▲
On se propose de tracer un segment de droite pixel par pixel. Pour ce faire, nous allons débuter à une extrémité du segment. Nous allons nous intéresser aux segments dont la direction est comprise entre Est et Sud-Est. L'idée de base, c'est d'avancer en abscisse de 1 pixel à chaque pas. Deux possibilités : soit on reste sur la même ligne (on va plein Est pour cette itération), soit on passe à la ligne du dessous (on va au Sud-Est). Ça, c'est pour le cas où on dessine de gauche à droite et de haut en bas, avec un écart en abscisse supérieur à l'écart en ordonnée. Ce qui fait qu'il est valable pour un huitième de cercle. Il suffit de changer le sens de parcours et d'inverser abscisse et ordonnée pour aller où l'on veut.
II-4-b. Le comment▲
- On se fixe une variable particulière : l'écart à la droite optimale.
- On initialise cet écart à la longueur à parcourir en abscisse. C'est la valeur maximale que peut prendre cet écart : le segment de droite orienté Sud-Est.
- On dessine le point où l'on est.
- On ajoute 1 à son abscisse. On va regarder si on dessine sur la même ligne ou sur la ligne d'en dessous.
- On ôte à l'écart la longueur à parcourir en ordonnée. Cela donne l'erreur que l'on commet.
- Si l'écart est inférieur à la longueur à parcourir en ordonnée, c'est qu'on est trop haut. On ajoute 1 à l'ordonnée du point sur lequel on est. On ajoute à l'écart la longueur à parcourir en ordonnée.
- Et on recommence.
II-4-c. La fonction▲
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; afficheLigne ;;
;;------------------------------------------------------------------;;
;; Dessine un segment de droite entre le point stocké dans EAX et ;;
;; celui stocké dans EBX (mot haut : X, mot bas : Y) avec la ;;
;; couleur contenue dans couleurPoint ;;
;; ES doit contenir le descripteur de segment de l'écran. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Y2: dd 0
afficheLigne:
push esi
push eax
push ebx
push ecx
push edx
push edi
cmp eax, ebx ;On travaille dans le sens des abscisses positives
jbe .ordreOK
xchg eax, ebx ;On inverse si ce n'est pas le cas
.ordreOK:
mov [Y2], ebx ;On stocke le point d'arrivée
sub ebx, eax ;On calcule les longueurs à parcourir en X et Y
mov cx, bx ;deltaY
shr ebx, 16 ;deltaX
or bx, bx ;Si deltaX est nul, on sera sur une ligne verticale
jz .ligneVerticale
or cx, cx ;Si deltaY est nul, on sera sur une ligne horizontale
jnz .test_deltaY
mov cx, bx ;CX contient le nombre de pixels à afficher
mov ebx, 0x10000;Incrément de 1 pixel vers la droite
jmp .avance_H_V
.ligneVerticale:
mov ebx, 0x00001;Incrément de 1 pixel vers le bas
.avance_H_V:
call metPixel ;Affichage du pixel
add eax, ebx ;Incrément
loop .avance_H_V
.fin_afficheLigne:
call metPixel ;Affichage du dernier pixel
pop edi
pop edx
pop ecx
pop ebx
pop eax
pop esi
ret
.test_deltaY:
push 0x10000 ;Incrément de 1 pixel vers la droite
push 1 ;Incrément de 1 pixel vers le bas
jns .suite ;Si deltaY est positif, on est standard
neg cx ;Sinon, on l'oppose, ainsi que l'incrément en Y
neg dword[esp]
.suite:
cmp bx, cx ;Si deltaY>deltaX
jae .principal
xchg bx, cx ;On échange deltaY et deltaX
mov edx, [esp + 4] ;Ainsi que les incréments
xchg edx, [esp]
mov [esp + 4], edx
.principal: ;Et on continue normalement
mov dx, bx ;DX contient l'écart
.boucle:
call metPixel
sub dx, cx ;On enlève deltaY
cmp dx, cx ;Si on est inférieur ou égal à deltaY
ja .affichage
add eax, [esp] ;On applique l'incrément en Y
add dx, bx ;On ajoute deltaX à l'écart
.affichage:
add eax, [esp + 4] ;On applique l'incrément en X
cmp eax, [Y2] ;Sommes-nous arrivés ?
jb .boucle
pop edx ;On s'en va proprement
pop edx
jmp .fin_afficheLigne
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin afficheLigne ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;C'est quand même sacrément plus propre que la version précédente. Le 32 bits permet quand même un joli coup de rabot en taille de code, ça fait plaisir. Et en général, un code plus court est un code plus rapide. Là , on pourrait calculer directement l'adresse mémoire pour faire mieux. S'il y a des amateurs, encore une fois, c'est avec plaisir. C'est assez simple à faire.
II-5. De La Bruyère▲
La fonction d'affichage des caractères a eu aussi droit à un coup de lifting.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; afficheCaractere ;;
;;------------------------------------------------------------------;;
;; Dessine un caractère au point stocké dans EAX (mot haut : X, ;;
;; mot bas : Y) avec la couleur contenue dans couleurPoint. ;;
;; ES doit contenir le descripteur de segment de l'écran. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
afficheCaractere:
push edx
push esi
push ecx
push eax
push ebx
mov ebx, eax ;EBX stocke le pixel courant
mov eax, ecx ;ECX contient le n° du caractère
mov ecx, 6 ;6 lignes par caractère
mul ecx
mov esi, Alphabet ; adresse de la lettre
add esi, eax ; si contient l'adresse de la lettre
.colonne:
push ecx
mov dl, 0b10000000
mov ecx, 8; On affiche 8 colonnes
lodsb
.ligne:
test al, dl
jz .suite
xchg eax, ebx
call metPixel
xchg eax, ebx
.suite:
shr dl, 1
add ebx, 0x10000
loop .ligne
pop ecx
inc ebx; passage à la ligne suivante
sub ebx, 8*0x10000
loop .colonne
pop ebx
pop eax
pop ecx
pop esi
pop edx
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fin afficheCaractere ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Ici aussi, on peut faire la même optimisation que précédemment. Si une âme charitable s'en sent le courage...
II-6. Du neuf▲
Il faut toujours faire du neuf, même pas beaucoup, pour dire. Là , ce sera une première approche d'édition de texte avec nos fonctions. On tape au clavier, on appuie sur Entrée, hop ça s'affiche.
II-7. Du code▲
L'ensemble du code se trouve ici : Paquet


