<-Précédent Retour à l'accueil Retour au niveau supéreur Suivant->

    Assembleur : reconstruction - modes graphiques

    Des modes graphiques

    Pour faire une sorte de séance de révision, nous allons retravailler les graphiques du chapitre 5. Reprenons tout le déroulement du programme :

    1. Saut au début, optionnel
    2. P'tit message pour ne pas perdre la main
    3. Demande d'informations sur le VESA BIOS EXTENSION
    4. Traitement de ces informations
    5. Choix du mode graphique
    6. Passage dans le mode choisi
    7. Affichage de points
    8. Affichage de droites
    9. Affichage d'un alphabet

  1. Saut au début, optionnel
  2. org 0x0000 ; Adresse de début .COM jmp start %include "affichageTexte.asm" start: ;On initialise Data Segment et Extra Segment à Code Segment
    call initialise_segments

    On s'en souvient, on est en binaire pur pour être chargé depuis un chargeur d'amorçage, on a gardé la directive org d'une inutilité certaine ici. On fait un saut pour inclure les fonctions d'affichage de texte au début. C'est un choix, absolument pas justifiable par autre chose que "c'est comme ça". On appelle une fonction "initialise_segments" qui ne sera jamais appelée qu'une fois. On pourrait la mettre en macro, mais nous verrons les macros plus tard. C'est donc encore en fonction.

    ;Initialisation des segments
    initialise_segments:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ax, 0x8000
    cli
    mov ss, ax
    mov sp, 0xF000
    sti
    ret
    ;Fin initialisation segments

  3. P'tit message pour ne pas perdre la main
  4. mov si, hello; met l'adresse de la chaîne à afficher dans le registre SI
    call affiche_chaine

    Bon, le message sera affiché, mais tel quel, on a d'excellentes chances de ne pas le voir, parce qu'on va afficher plus d'un écran par la suite. Il faut modifier le code qui viendra après pour le voir.

  5. Demande d'informations sur le VESA BIOS EXTENSION
  6. mov ax, 0x4F00 ; demande infos sur le pilote VESA VBE
    mov di, VESASignature
    int 10h
    cmp ax, 0x4F ; Si AL <> 0x4F, on n'a pas de VESA, donc fin. Si AH <> O, erreur, donc fin.
    jne fin

    VESASignature est défini dans la partie "données" :

    ;Informations du pilote VESA
    VESASignature: times 4 db 0; 'VESA', signature de 4 octets
    VESAVersion: dw 0; numéro de version de VBE
    OEMStringPtr: dd 0; Pointeur vers le nom de l'OEM
    Capabilities: dd 0; Possibilités de la carte graphique
    VideoModePtr: dd 0; Pointeur vers les modes accessibles
    TotalMemory: dw 0; Nombre de blocs mémoire de 64ko
    reserved: times 236 db 0; Complément à 256 octets, taille du bloc

    Ces informations, je ne les sors pas de mon chapeau, évidemment. Elle proviennent d'ici : VESA BIOS Extension (VBE) Core Functions Standard Version 2.0, tout ça parce que dans ma machine virtuelle, c'est VBE 2.0 qui est implanté.

  7. Traitement de ces informations
  8. Le traitement des informations VESA consiste principalement en deux choses : gérer les cas d'erreur et pousser plus avant les investigations. Pour les erreurs, on restera très simple puisqu'on sort au moindre problème. Pour le reste, voyons cela.

    Première chose, afficher le nom du constructeur :

    cmp ax, 0x4F ; Si AL <> 0x4F, on n'a pas de VESA, donc fin. Si AH <> O, erreur, donc fin.
    jne fin
    mov si, OEMStringPtr ; pointeur vers le nom de l'OEM stocké offset:segment
    lodsw; on charge l'adresse d'offset dans ax
    mov bx, ax ; BX contient l'adresse d'offset
    lodsw ; on charge l'adresse de segment dans ax
    mov si, bx ; SI pointe sur le nom de l'OEM
    push ds ; on sauvegarde DS
    mov ds, ax ; ds contient l'adresse de segment du nom de l'OEM
    call affiche_chaine
    pop ds ; on restaure DS
    mov cx, 18
    lignes_vides:
    mov si, retour_chariot
    call affiche_chaine
    loop lignes_vides

    Notez qu'on ajoute 18 lignes de retours chariot. C'est bête, c'est méchant et ça ne sert qu'à décaler l'affichage pour en avoir plus à visualiser sur l'écran suivant. Cette horreur devrait partir prochainement.

    Nous allons ensuite lire les informations de chaque mode supporté par notre interruption 0x10. Ca nous permettra de choisir un bon mode vidéo.

    mov si, VideoModePtr ; pointeur vers la liste des modes supportés
    lodsw ; on charge l'adresse d'offset dans ax
    mov cx, ax ; cx contient l'adresse d'offset
    lodsw ; on charge l'adresse de segment dans ax
    mov si, cx ; si pointe sur le premier mode supporté
    mov dx, ax ; dx contient l'adresse de segment
    lit_mode_suivant:
    push ds
    mov ds, dx ; ds contient l'adresse de segment de la liste des modes
    lodsw ;charge dans ax le mode
    pop ds
    cmp ax, 0xFFFF ; Fin de la liste
    je arret_modes
    mov cx, ax
    mov ax, 0x4F01 ; demande infos sur le mode VESA
    mov di, ModeAttributes
    int 0x10

    VideoModePtr est défini dans le bloc de données :

    ;Informations d'un mode vidéo
    ModeAttributes: dw 0; Attributs du mode
    WinAAttributes: db 0; Attibuts de la fenêtre A
    WinBAttributes: db 0; Attibuts de la fenêtre B
    WinGranularity: dw 0; Granularité de la fenêtre en ko
    WinSize: dw 0; Taille de la fenêtre en ko
    WinASegment: dw 0; Segment de la fenêtre A
    WinBSegment: dw 0; Segment de la fenêtre B
    WinFuncPtr: dd 0; Pointeur vers la fonction "de fenêtrage"
    BytesPerScanLine: dw 0; Octets par "scanline"
    XResolution: dw 0; Résolution horizontale
    YResolution: dw 0; Résolution vertical
    XCharSize: db 0; Largeur d'un caractère
    YCharSize: db 0; Hauteur d'un caractère
    NumberOfPlanes: db 0; Nombre de plans mémoire
    BitsPerPixel: db 0; Bits par pixel
    NumberOfBanks: db 0; Nombre de banques de style CGA
    MemoryModel: db 0; Type de modèle mémoire
    BankSize: db 0; Taille des banques de style CGA
    NumberOfImagePages: db 0; Nombre de pages image
    res1: db 0; Reservé
    RedMaskSize: db 0; Taille du masque rouge en couleur directe
    RedFieldPosition: db 0; Position du bit faible du masque rouge
    GreenMaskSize: db 0; Taille du masque vert en couleur directe
    GreenFieldPosition: db 0; Position du bit faible du masque vert
    BlueMaskSize: db 0; Taille du masque bleu en couleur directe
    BlueFieldPosition: db 0; Position du bit faible du masque bleu
    RsvdMaskSize: db 0; Taille du masque réservé en couleur directe
    RsvdFieldPosition: db 0; Position du bit faible du masque réservé
    DirectColorModeInfo: db 0; Attributs du mode de couleur directe
    res2: times 216 db 0; Complément à 256 octets, taille du bloc

    Le test d'erreurs :

    cmp ax, 0x4F ; Si AL <> 0x4F, la fonction n'est pas supportée, on se contentera du VGA. Si AH <> O, erreur, pareil.
    jne lit_mode_suivant
    test word [ModeAttributes], 0xF
    jz lit_mode_suivant

    On affiche les informations pertinentes du mode courant :

    ;On ecrit les modes
    mov di, hello ; on écrit dans hello
    mov ax, cx
    push cx
    mov ch, 3
    mov bl, 16
    call nombre_vers_chaine
    mov al, ':'
    stosb
    mov ch, 4
    mov bl, 10
    mov ax, [XResolution]
    call nombre_vers_chaine
    mov ax, ('*' << 8) + ' '
    stosw
    mov al, ' '
    stosb
    mov ax, [YResolution]
    call nombre_vers_chaine
    mov ax, ' '
    stosb
    mov ch, 2
    mov al, [BitsPerPixel]
    call nombre_vers_chaine
    mov al, ' '
    stosb
    mov ax, ';' ; on met 2 caractères d'un coup après la chaîne : un '\n' et le zéro terminal.
    stosw ; les caractères sont dépilés, c'est à dire qu'il faut placer le premier dans la zone basse
    pop cx
    push si ;sauve si sur la pile
    mov si, hello
    call affiche_chaine
    pop si ; on récupère si

    Les esprits chagrins remarqueront que j'écris sur l'emplacement hello. Alors, il est fait pour cela, et j'en suis parfaitement conscient. Vu le peu que j'écris, j'ai largement la place dans cet espace pour mettre ce qui m'intéresse.

    On va déborder un peu sur le paragraphe suivant : on va chercher quelle est la meilleure résolution à utiliser. On va la définir comme étant celle maximisant largeurEnPixels * hauteurEnPixels * TailleDeCodageDesCouleurs. Ca se fait dans la même boucle, c'est pour cela qu'on en parle maintenant.

    push dx
    mov ax, [XResolution]
    shr ax, 5
    push ax
    mov ax, [YResolution]
    shr ax, 3
    push ax
    mov al, [BitsPerPixel]
    xor ah, ah
    shr ax, 3
    pop bx
    mul bx
    pop bx
    mul bx
    pop dx
    cmp ax, [maxResol]
    jb lit_mode_suivant
    mov [maxResol], ax
    mov [mode_souhaite], cx
    jmp lit_mode_suivant

  9. Choix du mode graphique
  10. arret_modes:
    mov cx, [mode_souhaite] ; On s'enquiert du mode souhaité
    mov ax, 0x4F01 ; demande infos sur le mode VESA
    mov di, ModeAttributes
    int 0x10
    mov di, hello ; on écrit dans hello
    mov ax, cx
    push cx
    mov ch, 3
    mov bl, 16
    call nombre_vers_chaine
    mov al, ':'
    stosb
    mov ch, 4
    mov bl, 10
    mov ax, [XResolution]
    call nombre_vers_chaine
    mov ax, ('*' << 8) + ' '
    stosw
    mov al, ' '
    stosb
    mov ax, [YResolution]
    call nombre_vers_chaine
    mov ax, ' '
    stosb
    mov ch, 2
    mov al, [BitsPerPixel]
    call nombre_vers_chaine
    mov ax, 13 ; on met 2 caractères d'un coup après la chaîne : un '\n' et le zéro terminal.
    stosw ; les caractères sont dépilés, c'est à dire qu'il faut placer le premier dans la zone basse
    pop cx
    mov si, hello
    call affiche_chaine

    mov al, [BitsPerPixel]
    shr al, 3
    mov byte [octetsParPixel], al
    mov ax, [WinASegment]
    or ax, ax ; on teste l'adresse du segment de la fenêtre. Si elle est nulle, on passe en mode 0x13
    jnz adresse_OK
    adresse_mode_13h:
    mov word [mode_souhaite], 0x0013 ; infos du mode 0x13, le mode VGA
    mov word [WinASegment], 0xA000
    mov word [YResolution], 200
    mov word [XResolution], 320
    mov byte [octetsParPixel], 1
    adresse_OK:
    mov di, hello ; met l'adresse de la chaîne à lire dans le registre SI
    call lit_chaine ; On attend l'utilisateur pour nettoyer l'écran

  11. Passage dans le mode choisi
  12. mov ax, 0x4F02
    mov bx, [mode_souhaite]
    int 0x10 ; Changement de mode vidéo
    call nettoyage_ecran

    1. Nettoyage de l'écran
    2. Derrière ce terme ménager se cache juste le remplissage de l'intégralité de l'écran avec une couleur de fond. Comme d'habitude, l'idée n'est pas l'optimisation, le code super rapide comme l'ont tous les logiciels spécialisés. L'idée est d'avoir d'abord un code fonctionnel, créé au plus évident. L'algorithme que je vous propose ne prend en compte aucun prérequis autre qu'un mode VESA disposant d'une fenêtre A en mode écriture. Enfin, il me semble.

      On écrit dans la mémoire vidéo par bloc de 64 ko. L'idée est donc de remplir tous ces blocs par la couleur de fond.

      nettoyage_ecran:
      push di
      push es
      push ax
      push bx
      push cx
      push dx
      mov es, [WinASegment]; On lit l'adresse de départ de la mémoire vidéo

      mov cx, [YResolution]
      mov ax, [XResolution]
      mul cx ; Nombre de points total
      mov ax, dx
      mov cl, [octetsParPixel]
      mul cl
      mov cx, ax
      xor dx, dx
      xor bh, bh
      xor bl, bl
      boucle_fenetres:
      push cx
      mov ax, 0x4F05
      int 0x10 ; Changement de fenêtre
      mov cx, [WinSize]
      shl cx, 9 ;Passage de ko en mots.
      xor di, di
      .point:
      push cx
      push bx
      mov cl, [octetsParPixel]
      xor ch, ch
      mov bx, couleur_defaut
      .couleur:
      inc bx
      mov al, byte [bx]
      stosb
      loop .couleur
      pop bx
      pop cx
      loop .point
      inc dx
      pop cx
      loop boucle_fenetres
      .depile:
      xor dx, dx
      mov [bloc_courant], dl
      mov ax, 0x4F05
      int 0x10 ; Changement de fenêtre
      pop dx
      pop cx
      pop bx
      pop ax
      pop es
      pop di
      ret
      ;Fin nettoyage_ecran

  13. Affichage de points
  14. Avec cette fonction, vous pouvez dorénavant dessiner ce que vous voulez où vous le voulez, en, normalement, n'importe quel mode VESA ! L'idée de base reste de mettre, octet par octet, la couleur spécifiée à l'adresse couleurPoint dans la mémoire vidéo, et de donner le bon numéro de plage. Ce bon numéro de plage est toujours basé sur la sainte formule : ordonnée * octetsParLigne + abcsisse. Je suppose que le débordement de la multiplication me donne le numéro convoité.

    ;fonction affiche_point : on est déjà dans un mode graphique
    ;BX : Coordonnée X du point
    ;AX : Coordonnée Y du point
    affiche_point:
    push bx ; On sauve les registres qu'on va manipuler
    push cx
    push es
    push di
    push dx
    push ax
    mov cx, word [BytesPerScanLine]
    mul cx
    mov di, ax
    push dx
    mov ax, bx
    xor ch, ch
    mov cl, byte [octetsParPixel]
    mul cx
    add di, ax
    pop dx
    .change_fenetre:
    mov ax, 0x4F05
    xor bh, bh
    xor bl, bl
    int 0x10 ; Changement de fenêtre
    mov es, [WinASegment] ; On va dans la mémoire vidéo
    mov bx, couleurPoint
    .couleur:
    inc bx
    mov al, byte [bx]
    stosb
    loop .couleur
    pop ax ; On restaure les registres manipulés
    pop dx
    pop di
    pop es
    pop cx
    pop bx
    ret
    ;fin de affiche_point

  15. Affichage de droites
  16. On avait déjà parlé de l'algorithme de Bresenham, le revoici à l'identique. Il a été mis à jour pour refléter l'inversion des paramètres de affiche_point. On met sur la pile X1, puis Y1, puis X2 et enfin Y2.

    ;fonction affiche_ligne : on est déjà dans un mode graphique
    affiche_ligne:
    jmp depart_affiche_ligne
    Y2: dw 0
    X2: dw 0
    Y1: dw 0
    X1: dw 0
    deltaX: dw 0
    deltaY: dw 0
    incX: dw 0
    incY: dw 0
    e: dw 0
    depart_affiche_ligne:
    push si
    push ax
    push bx
    push cx
    push dx
    push di
    push es
    mov ax, sp
    mov si, ax
    add si, 16 ; SI pointe sur Y2
    mov di, Y2
    mov ax, ds
    mov es, ax
    mov ax, ss
    mov ds, ax
    mov cx, 4
    rep movsw
    mov ax, es
    mov ds, ax
    mov ax, [X2]
    mov bx, [X1]
    sub ax, bx
    mov [deltaX], ax
    mov cx, [Y2]
    mov bx, [Y1]
    sub cx, bx
    mov [deltaY], cx
    or ax, ax ; test deltaX
    jnz test_deltaX_positif
    or cx, cx ; test deltaY
    jnz test_deltaY_deltaX_nul
    fin_affiche_ligne:
    mov bx, [X2]
    mov ax, [Y2]
    call affiche_point
    pop es
    pop di
    pop dx
    pop cx
    pop bx
    pop ax
    pop si
    ret

    deltaX_positif:
    or cx, cx
    jnz test_deltaY_deltaX_positif
    ;vecteur horizontal vers la droite
    mov cx, [deltaX]
    mov word [incX], 1
    mov word [incY], 0
    jmp ligne_H_V

    test_deltaY_deltaX_nul:
    ;cx contient deltaY
    mov word [incY], 1
    mov word [incX], 0
    cmp cx, 0
    jns ligne_H_V
    neg cx
    mov word [incY], -1
    ligne_H_V:
    mov bx, [X1]
    mov ax, [Y1]
    avance_H_V:
    call affiche_point
    add bx, [incX]
    add ax, [incY]
    loop avance_H_V
    jmp fin_affiche_ligne

    test_deltaX_positif:
    cmp ax, 0
    jns deltaX_positif
    or cx, cx ; CX contient DeltaY
    jnz test_deltaY_deltaX_negatif
    ;vecteur horizontal vers la gauche
    mov cx, [deltaX]
    neg cx
    mov word [incX], -1
    mov word [incY], 0
    jmp ligne_H_V

    charge_registres:
    shl cx, 1
    shl ax, 1
    mov [deltaY], cx
    mov [deltaX], ax
    mov bx, [X1]
    mov ax, [Y1]
    ret

    charge_e_deltaX_et_cmp_X2:
    mov [e], ax
    call charge_registres
    mov cx, [X2]
    ret

    charge_e_deltaY_et_cmp_Y2:
    mov [e], cx
    call charge_registres
    mov cx, [Y2]
    ret

    affiche_et_charge_eY:
    call affiche_point
    add ax, [incY]
    mov dx, [e]
    ret

    affiche_et_charge_eX:
    call affiche_point
    add bx, [incX]
    mov dx, [e]
    ret

    octants1_et_4:
    call charge_e_deltaX_et_cmp_X2
    depart_boucle1:
    call affiche_et_charge_eX
    cmp bx, cx
    je fin_affiche_ligne
    sub dx, [deltaY]
    cmp dx, 0
    jns X_pret1
    add ax, [incY]
    add dx, [deltaX]
    X_pret1:
    mov [e], dx
    jmp depart_boucle1

    deltaY_positif_deltaX_negatif:
    neg ax
    deltaY_positif_deltaX_positif:
    mov word [incY], 1
    ;deltaY > 0, deltaX > 0
    cmp ax, cx
    jae octants1_et_4
    neg ax
    call charge_e_deltaY_et_cmp_Y2
    depart_boucle2_et_3:
    call affiche_et_charge_eY
    cmp ax, cx
    je fin_affiche_ligne
    add dx, [deltaX]
    cmp dx, 0
    jns X_pret2_et_3
    add bx, [incX]
    add dx, [deltaY]
    X_pret2_et_3:
    mov [e], dx
    jmp depart_boucle2_et_3

    octant5:
    call charge_e_deltaX_et_cmp_X2
    depart_boucle5:
    call affiche_et_charge_eX
    cmp bx, cx
    je fin_affiche_ligne
    sub dx, [deltaY]
    cmp dx, 0
    js X_pret5
    add ax, [incY]
    add dx, [deltaX]
    X_pret5:
    mov [e], dx
    jmp depart_boucle5

    octant8:
    neg cx
    call charge_e_deltaX_et_cmp_X2
    depart_boucle8:
    call affiche_et_charge_eX
    cmp bx, cx
    je fin_affiche_ligne
    add dx, [deltaY]
    cmp dx, 0
    jns X_pret8
    add ax, [incY]
    add dx, [deltaX]
    X_pret8:
    mov [e], dx
    jmp depart_boucle8

    test_deltaY_deltaX_positif:
    mov word [incX], 1
    cmp cx, 0
    jns deltaY_positif_deltaX_positif
    ;deltaY < 0, deltaX > 0
    mov word [incY], -1
    neg cx
    cmp ax, cx
    jae octant8
    neg cx
    jmp octants6_et_7
    test_deltaY_deltaX_negatif:
    mov word [incX], -1
    cmp cx, 0 ; cx contient deltaY
    jns deltaY_positif_deltaX_negatif
    ;deltaY < 0, deltaX < 0
    mov word [incY], -1
    cmp ax, cx ; ax contient deltaX
    jbe octant5
    neg ax
    octants6_et_7:
    call charge_e_deltaY_et_cmp_Y2
    depart_boucle6_et_7:
    call affiche_et_charge_eY
    cmp ax, cx
    je fin_affiche_ligne
    add dx, [deltaX]
    cmp dx, 0
    js X_pret6_et_7
    add bx, [incX]
    add dx, [deltaY]
    X_pret6_et_7:
    mov [e], dx
    jmp depart_boucle6_et_7
    ;AFFICHE_LIGNE ENDP

  17. Affichage d'un alphabet
  18. Voici la grande nouveauté de ce chapitre : un alphabet ! En mode graphique, oui madame, dessiné par mes soins, à la va-comme-j'te-pousse. Je l'ai fait parce qu'il me semble que bientôt, nous n'en aurons plus. Et puis, ça permettra d'afficher facilement des 'é', 'ç' et autres 'à'. Dès que j'aurai mis ces caractères, bien sûr. La fonction est très simple : on lui donne le numéro du caractère, avec A = 1, dans cx. ax contient l'ordonnée du coin supérieur gauche du caractère et bx son abscisse.

    affiche_caractere:
    push bx
    push dx
    push si
    push cx
    push ax
    push ax
    mov ax, cx
    mov cx, 6
    mul cx
    mov si, Alphabet; adresse de la lettre
    add si, ax; si contient l'adresse de la lettre
    .colonne:
    pop ax
    push cx
    mov dl, 0b10000000
    mov cx, 8; On affiche 8 colonnes
    .ligne:
    push ax
    mov al, [si]; On charge l'octet à afficher
    test al, dl
    jz .suite
    pop ax
    call affiche_point
    push ax
    .suite:
    shr dl, 1
    inc bx
    pop ax
    loop .ligne
    pop cx
    inc ax; passage à la ligne suivante
    sub bx, 8
    push ax
    inc si
    loop .colonne
    pop ax
    pop ax
    pop cx
    pop si
    pop dx
    pop bx
    ret