Espace d'Asher256

Optimisation graphique bas niveau et gestion de la mémoire en QuickBasic





Écrit par Asher256, en Septembre 2001
URL: http://qbworld.asher256.com

Ce tutoriel vous enseignera les techniques pour optimiser votre code QuickBasic.

Les routines PSET et POINT : des performances limitées

Avant toute chose, évitez d’utiliser les modes graphiques SCREEN 7 ou SCREEN 12, qui sont aujourd’hui obsolètes. Privilégiez le mode SCREEN 13 (résolution de 320×200 pixels en 256 couleurs).

Les routines PSET natives de QuickBasic manquent d’efficacité. Si vous tentez de simuler un effacement d’écran (CLS) en redessinant chaque point avec PSET, l’affichage se fera pixel par pixel de manière très lente.

Exemple illustrant la lenteur de la routine PSET :

DEFINT A-Z:SCREEN 13
t! = TIMER

FOR c = 1 TO 10         ' Changement de couleur
        FOR y = 0 TO 199
                FOR x = 0 TO 319
                        PSET (x,y),c+50
                NEXT
        NEXT
NEXT

A$ = INPUT$(1)

SCREEN 0 : WIDTH 80
PRINT "Temps d'affichage : "; TIMER - t!; " secondes."

Voici une approche alternative bien plus performante utilisant les accès mémoire :

DEFINT A-Z              ' Utilisation systématique des INTEGER pour la rapidité
DIM offset AS LONG      ' Type LONG car l'adresse de l'offset dépasse 32767
SCREEN 13

DEF SEG = &HA000        ' Pointage du segment de la mémoire vidéo
t! = TIMER
FOR c = 1 TO 3          ' Changement de couleur
        FOR offset = 0 TO 63999
                POKE offset, c
        NEXT
NEXT
DEF SEG                 ' Restauration du segment par défaut de QuickBasic
A$ = INPUT$(1)

SCREEN 0 : WIDTH 80
PRINT "Temps d'affichage : "; TIMER - t!; " secondes."

Sur un processeur Pentium à 100 MHz, le premier exemple requiert environ 3,6 secondes pour vider l’écran, tandis que le second n’a besoin que de 0,8 seconde. Une fois le code source compilé en binaire autonome (*.EXE), l’exécution devient quasi instantanée (moins de 0,2 seconde).

Pourquoi la routine PSET de QuickBasic est-elle si lente ? C’est parce qu’elle effectue de nombreuses vérifications en arrière-plan : validation de la résolution active, détermination du format d’affichage et vérification du nombre maximal de couleurs (2, 16, 256, etc.). Cette surcharge logicielle propre aux instructions de haut niveau pénalise les performances. Pour contourner cela, il est possible d’intégrer du code assembleur via l’instruction CALL ABSOLUTE ou en liant directement des modules objets gérés par TASM ou MASM au format *.QLB.

Analysons à présent le fonctionnement technique de notre effacement d’écran via la commande POKE. Le tampon vidéo (framebuffer) est localisé à l’adresse mémoire &HA000:0000. La valeur &HA000 représente le segment, que l’on sélectionne à l’aide de l’instruction DEF SEG. L’offset (l’adresse relative) commence à 0 et se termine à 63999. L’écran en mode SCREEN 13 comptant 320×200 pixels, cela représente un total de 64000 emplacements distincts. L’indexation débutant à 0, la dernière adresse est naturellement égale à 64000 – 1, soit 63999.

Pour afficher un point isolé à l’écran, il faut calculer son offset précis à l’aide d’une formule simple : multiplier la coordonnée Y par la résolution horizontale de l’écran (320), puis y ajouter la coordonnée X. Voici l’implémentation de cette routine :

DEFINT A-Z      ' Utilisation des INTEGER pour optimiser les calculs
SUB Opset (x AS INTEGER,y AS INTEGER,couleur AS INTEGER)

        DIM offset AS LONG      ' L'offset pouvant dépasser 32767, le type LONG est requis
        DEF SEG = &HA000        ' Pointage du segment de la mémoire vidéo

        offset = x + (y * 320)  ' Calcul de l'adresse relative via la formule
        POKE offset, couleur    ' Écriture directe de la couleur en mémoire

        DEF SEG                 ' Restauration du segment par défaut de QuickBasic
END SUB

Après avoir intégré cette procédure (SUB) dans votre fichier *.BAS, vous pouvez l’invoquer ainsi :

Opset 100,100,15

Un pixel s’affichera aux coordonnées spécifiées. Vous disposez maintenant d’un équivalent rapide à PSET pour le mode SCREEN 13.

Bien que l’opération x + (y * 320) soit efficace, elle peut s’avérer insuffisante pour les exigences d’un moteur de jeu vidéo en temps réel. Pour atteindre des performances optimales, il est recommandé de transcrire cette routine en langage assembleur.

Il existe d’ailleurs une astuce pour optimiser l’opération y * 320 en remplaçant l’instruction de multiplication (MUL), relativement lourde, par des décalages de bits vers la gauche (SHL).

Le nombre 320 n’étant pas une puissance de 2, l’astuce consiste à décomposer la multiplication :

MOV AX, y        ; Chargement de la coordonnée Y dans AX
MOV BX, ax       ; Duplication de Y dans BX
SHL BX,6         ; Multiplie BX par 64  (2^6)
SHL AX,8         ; Multiplie AX par 256 (2^8)
ADD AX, BX       ; AX = (Y * 256) + (Y * 64) = Y * 320
MOV BX, x        ; Chargement de la coordonnée X dans BX
ADD AX, BX       ; AX = (Y * 320) + X (L'offset final est dans AX)
                 ; Il ne reste plus qu'à configurer le registre ES

Puisque 320 équivaut à 256 + 64, cette décomposition est tout à fait exacte. Prenons un exemple concret :

offset = 4 + (2 * 320)
       = 4 + 640
       = 644

Appliquons la méthode par décalage de bits (SHL) :

offset = 4 + (2 * 256) + (2 * 64)
       = 4 + 512 + 128
       = 644

Le résultat est strictement identique, mais l’utilisation de l’instruction SHL permet d’économiser des cycles processeur précieux par rapport à l’instruction MUL, dont l’exécution matérielle reste plus longue.

Pour remplacer l’instruction POINT de QuickBasic (qui lit la couleur d’un pixel à l’écran), vous pouvez appliquer la même formule de calcul d’offset associée à l’instruction PEEK : « couleur = PEEK(offset) ».

Ces routines d’optimisation de bas niveau s’avèrent indispensables pour concevoir des algorithmes rapides de tracés de lignes, de cercles, ou pour développer une routine d’affichage de sprites (PUT) performante. Si vous débutez, l’utilisation de l’instruction POKE en QuickBasic offre déjà un excellent compromis, bien que rien ne puisse égaler la vélocité pure d’un code écrit en assembleur.

Le choix des types de variables : SINGLE, LONG, INTEGER, DOUBLE

Une question essentielle se pose lors de la phase de conception : quel type de variable privilégier pour optimiser la rapidité d’exécution ?

Sachez qu’il faut impérativement bannir l’usage des types à virgule flottante tels que SINGLE ou DOUBLE au sein de vos boucles critiques, sous peine de ralentir considérablement votre application.

Utilisez prioritairement le type INTEGER. Stocké sur 2 octets, il s’avère simple et extrêmement rapide à traiter par le processeur. Le type LONG (4 octets) ne doit être mobilisé que lorsque vos valeurs risquent de dépasser la limite supérieure des entiers signés (32767). Dans le cadre du développement de jeux vidéo standard sur cette plateforme, vos coordonnées et indices dépasseront rarement ce seuil.

Pour forcer l’interprétation de toutes vos variables non déclarées en tant qu’INTEGER par défaut, il vous suffit de placer cette directive au tout début de votre code source :

DEFINT A-Z