<-Précédent | Retour à l'accueil | Contact : etienne"point"sauvage"at"gmail.com | Suivant-> |
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 !
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é, si vous avez d'autres retours, dites-le moi, y'a mon adresse juste au-dessus.
Donc la suite se déroulera sur VirtualBox.
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 ; base
C'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'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], ah
On fait un usage intensif de la structure d'un descripteur de segment, donnée au chapitre 8 ou 9. 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 vertical %define BitsPerPixel ModeAttributes + 25;: db 0; Bits par pixel %define FlatMemory ModeAttributes + 40
Parce 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 la place d'exécuter du code. Plutôt que de faire des chargements de secteur, de vérifier qu'on a bie 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éo
Détail pendant que j'y pense : le binaire peut aussi se suffixer par 'b'.
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 avz 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, on est obligés. C'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 et le restaurer. Pensez-y, on peut perdre des heures bêtement.
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.
L'idée de base, c'est d'avancer en abscisse de 1 pixel à chaque fois. Deux possiblités : soit on reste sur la même ligne, soit on passe à la ligne du dessous. Ca, 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 1/8ème de cercle. Il suffit de changer le sens de parcours et d'inverser abscisse et ordonnée pour aller où l'on veut.
On se fixe une variable particulière, l'écart à la droite optimale. On initialise cet écart à la longueur à parcourir en abscisse. On dessine le point où l'on est. On ajoute 1 à son abscisse. On ôte à l'écart la longueur à parcourir en ordonnée. 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. Et on recommence.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; 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 de 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 longueur à 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.
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...
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.
L'ensemble du code, depuis le chapitre 11, se trouve ici :
Tuto11