<-Précédent Retour à l'accueil Contact : etienne"point"sauvage"at"gmail.com Suivant->
  1. Des émulateurs
  2. Du Linear Frame Buffer
  3. Du dessin
  4. De Bresenham
    1. L'idée
    2. Le comment
    3. La fonction
  5. De La bruyère
  6. Du neuf
  7. Du code

    Assembleur : VESA en mode protégé

    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 !

  1. Des émulateurs
  2. 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.

  3. Du Linear Frame Buffer
  4. 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'.

  5. Du dessin
  6. 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.

  7. De Bresenham
  8. 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.

    1. L'idée
    2. 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.

    3. Le comment
    4. 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.

    5. La fonction
    6. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;; 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.

  9. De La Bruyère
  10. 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...

  11. Du neuf
  12. 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.

  13. Du code
  14. L'ensemble du code, depuis le chapitre 11, se trouve ici :
    Tuto11