Partie 2 : FrameBuffer

Développement amateur sur Nintendo DS Partie 2

La partie 1 de cette série de tutoriaux expliquait comment compiler et exécuter un programme pour afficher \'Hello World\' sur l\'écran.

Cette partie, la seconde, va vous permettre de dessiner sur un des écrans de la DS en utilisant le mode framebuffer. Chaque écran de la DS peut utiliser une grande variété de mode vidéo. Chaque mode a ses avantages et désavantages mais je vais utiliser dans ce tutorial le mode framebuffer car c\'est la façon la plus simple de dessiner directement sur l\'écran.

Framebuffer

Le \'framebuffer\' est un mode graphique où l\'écran est mappé dans une portion de la mémoire. Ecrire des données dans cette portion de mémoire revient à faire apparaître cette donnée sur l\'écran. Dans le mode que je vais utiliser, chaque pixel de l\'écran est représenté par 2 octets de données. Cela correspond en langage C à un unsigned integer, type de donnée 16 bits. La donnée que nous allons écrire en mémoire est la couleur du pixel au format 555 (5 bits par composante couleur Rouge, Vert et Bleu).

Mais, nous n\'avons pas à convertir manuellement nos couleurs car il y a une macro nommée \'RGB15\' qui nous donne la conversion en rouge, vert et bleu pour chaque pixel. Chaque composante rouge, verte et bleu est un nombre compris entre 0 et 31. 0 correspond à aucune couleur pour cette composante et 31 le maximum de couleur. Voici quelques exemples :

RGB15Couleur
RGB15(31,0,0)Rouge
RGB15(0,31,0)Vert
RGB15(0,0,31)Bleu
RGB15(0,0,0)Noir
RGB15(31,31,31)Blanc

Ce petit morceau de code montre comment remplir l\'écran en bleu en utilisant un pointeur sur le début de la mémoire en mode framebuffer :

uint16* framebuffer = ...;
for(int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; ++i)
    *framebuffer++ = RGB15(0,0,31);

Ecrire à l\'adresse du framebuffer va automatiquement afficher le pixel à l\'écran. La chose la plus sympathique avec ce mode, c\'est que vous pouvez affichez ce que vous voulez sur l\'écran, n\'importe où sur l\'écran, et la DS utilise alors directement l\'accélération hardware du mode 2D pour l\'afficher.

La contre partie de ce mode est que vous devez tout faire par vous même. Il n\'y a pas de \'sprites\', tiled maps, scrolling, etc sauf si vous le codez vous même. Les autres modes graphiques de la DS sont donc plus appropriés pour ce genre de choses et seront abordés dans d\'autres tutoriaux. Cependant, ce mode est très flexible et permet de se familiariser avec le hardware.

Ecrans

Du point de vue hardware, il y a 2 écrans sur la DS. Un en haut et un en bas. Seul l\'écran du bas est un écran tactile.

Du point de vue programmation, il y a aussi 2 écrans. Un écran \'principal\' et un écran \'secondaire\'. L\'un comme l\'autre de ces 2 écran accessibles par programmation peut être redirigé vers l\'un des deux écrans du point de vue hardware. Cet exemple ne va utiliser qu\'un seul écran, l\'écran principal, et il va être redirigé vers l\'écran du haut du point de vue hardware.

Pour définir le mode de l\'écran principal, nous utilisons une fonction nommée \'videoSetMode\'. Il peut y avoir plus d\'un framebuffer par écran hardware. Cela permet de réaliser certaines techniques comme le \'double buffering\' ou le \'page flipping\'. Nous n\'allons utiliser qu\'un seul framebuffer à la fois, donc nous utiliserons le mode MODE_FB0.

videoSetMode(MODE_FB0);

La localisation mémoire du framebuffer est composée d\'un certain nombre d\'emplacements nommés \'VRAM\' commençant par la lettre \'A\'. Nous devons dire au système graphique quelle VRAM nous utiliserons pour le framebuffer. Nous supposons que nous utilisons le premier, VRAM_A :

vramSetBankA(VRAM_A_LCD);

Dessiner une forme

La forme que nous allons dessiner à l\'écran sera un carré tout simple d\'une seule couleur. Pour réaliser ce carré, nous allons faire une fonction qui prend en entrée les coordonnées x et y du carré, la couleur et le pointeur vers le début du framebuffer utilisé pour dessiner :

void draw_shape(int x, int y, uint16* buffer, uint16 color)
{
    buffer += y * SCREEN_WIDTH + x;
    for(int i = 0; i < shape_height; ++i)
    {
        uint16* line = buffer + (SCREEN_WIDTH * i);
        for(int j = 0; j < shape_width; ++j)
        {
            *line++ = color;
        }
    }
}

Le framebuffer est disposé en lignes en mémoire. Aussi, si l\'écran étaient de 200 pixels de large, les 200 premiers uint16 dans le framebuffer représenteraient la première ligne sur l\'écran. Les 200 suivant seraient la deuxième ligne, etc...

La fonction, draw_shape, calcule d\'abord à quel endroit le premier pixel doit être affiché. Notez que SCREEN_WIDTH et SCREEN_HEIGHT sont des macros livrés dans \'libnds\' qui donne la largeur et hauteur de l\'écran en pixels.

Cette fonction dessine chaque ligne de la forme en mettant la donnée contenant le pixel de couleur à la bonne place dans le framebuffer.

shape_height et shape_width sont des variables statiques pour permettre de tester facilement des changements de hauteur et largeur de la forme :

static int shape_width = 10;
static int shape_height = 10;

Déplacement de la forme

Pour donner l\'impression que la forme se déplace à l\'écran, nous devons effacer la forme à sa position actuelle et la redessiner à sa nouvelle position. On réalise cette opération en gardant en mémoire les postions \'x\' et \'y\' avant et après le déplacement.

static int old_x = 0;
static int old_y = 0;
static int shape_x = 0;
static int shape_y = 0;

L\'affichage de la forme devient alors très simple, il faut d\'abord appeler \'draw_shape\' avec la couleur du fond (dans notre cas , noir) pour l\'effacer en utilisant les valeurs des anciennes positions old_x et old_y, et l\'appeler de nouveau avec les valeurs des nouvelles \'x\' et \'y\' avec la couleur de la forme (rouge dans notre cas) :

draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));

Notez que le framebuffer passé à notre fonction \'draw_shape\' est \'VRAM_A\', le même framebuffer que nous avons redirigé vers l\'écran principal juste avant.

Pas terrible cependant ...

Une fonction \'main\' simple pour réaliser le calcul des positions et l\'affichage ressemble à quelque chose comme cela :

int main(void)
{
    powerON(POWER_ALL);
    videoSetMode(MODE_FB0);
    vramSetBankA(VRAM_A_LCD);

    while(1)
    {
        old_x = shape_x;
        old_y = shape_y;
        shape_x++;
        if(shape_x + shape_width >= SCREEN_WIDTH)
        {
            shape_x = 0;
            shape_y += shape_height;
            if(shape_y + shape_height >= SCREEN_HEIGHT)
            {
                shape_y = 0;
            }
        }
        draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
        draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
    }

Si vous exécutez le code ci-dessus, vous apercevrez des formes \'inclinées\' qui défilent tout au long de l\'écran comme dans cette Capture d\'écran sous Dualis.

Interruption Vertical Blank : VBL

La raison de ces formes inclinées dans l\'essai précédent provient de la façon dont l\'écran fonctionne. Le hardware redessine l\'écran toutes les 1/60ème de seconde. Il réalise toute cela pixel par pixel, ligne par ligne, et en copiant le contenu du framebuffer pour ce pixel donnée vers le pixel sur l\'écran.

Pendant que cela arrive, dans notre \'main\', nous changeons le contenu du framebuffer qui a été affiché à l\'écran. Donc, si le hardware affiche notre forme juste avant que nous l\'effacions, elle ne sera pas effacé immédiatement. Si nous affichons notre nouvelle forme, juste avant que le hardware ne le fasse, elle aura alors des morceaux de l\'ancienne forme et des morceaux de la nouvelle.

Heureusement, le hardware a un moyen de nous prévenir qu\'il a finit d\'afficher l\'écran. Ce moyen est appelé la \'Vertical Blank Interrupt\'. Nous pouvons solliciter une fonction qui est appelée lorsque cela survient.

Une \'interruption\' est un mécanisme hardware qui \'interrompt\' ce que nous faisons à un instant t (l\'exécution dans une boucle while du \'main\' par exemple) pour appeler rapidement une fonction pour réaliser autre chose. Quand l\'interruption se termine, l\'activité précédente contenue au moment où elle s\'était interrompue.

Pour pallier au problème rencontré juste avant, nous devons écrire dans le framebuffer à un moment à un moment où le hardware n\'est pas en train de copier son contenu sur l\'écran. Le meilleur moment pour le fait se trouve pendant la VBL.

Paramétrer les interruptions

Un tutorial sur les interruptions est disponible et décrit en détail leur fonctionnement. Cependant, je vais décrire brièvement ce qui se passe dans le code de l\'interruption.

Tout d\'abord, nous avons besoin de dire à la Nintendo DS quelle fonction nous souhaitons appeler lorsque l\'interruption survient :

void InitInterruptHandler()
{
    IME = 0;
    IRQ_HANDLER = on_irq;
    IE = IRQ_VBLANK;
    IF = ~0;
    DISP_SR = DISP_VBLANK_IRQ;
    IME = 1;
}

Dans cet exemple de code, nous ne souhaitons déclencher que les interruptions de type VBlank et la fonction \'on_irq\' est appelée lorsque cette interruption se produit.

La fonction on_irq va réaliser l\'affichage du framebuffer que nous faisions auparavant dans le \'main\':

void on_irq()
{
    if(IF & IRQ_VBLANK)
    {
        draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
        draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));

        // Indique à la DS que nous gerons les interruptions VBLANK
        VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
        IF |= IRQ_VBLANK;
    }
    else
    {
        // Ignore toutes les autres interruptions
        IF = IF;
    }
}

La partie principale de la fonction réalise l\'affichage fait dans le \'while\' du \'main\' auparavant. Le reste concerne la gestion des interruptions.

Nous avons aussi besoin de dire au hardware de la DS que nous gérons l\'interruption VBLANK. C\'est obligatoire pour l\'appel à \'swiWaitForVBlank\' que nous utiliserons ensuite et j\'expliquerais pourquoi. Le code pour réaliser cela est :

// Indique à la DS que nous gerons les interruptions VBLANK
VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
IF |= IRQ_VBLANK;

Pas encore ça ...

Nous pouvons enlever le code du \'main\' maintenant et il devient donc :

int main(void)
{
    powerON(POWER_ALL);
    videoSetMode(MODE_FB0);
    vramSetBankA(VRAM_A_LCD);
    InitInterruptHandler();
    while(1)
    {
        old_x = shape_x;
        old_y = shape_y;
        shape_x++;
        if(shape_x + shape_width >= SCREEN_WIDTH)
        {
            shape_x = 0;
            shape_y += shape_height;
            if(shape_y + shape_height >= SCREEN_HEIGHT)
            {
                shape_y = 0;
            }
        }
    }

Malheureusement, l\'exécution nous montre un problème. Cette capture d\'écran de Dualis montre que la forme apparait plusieurs fois à l\'écran ayant de temps en temps l\'apparence d\'un échiquier.

Heureusement, la raison en est connu. Le hardware appelle la fonction on_irq chaque 1/60ème de second quand une interruption vertical blank intervient. C\'est lorsque la forme est effacée puis réaffichée à l\'écran.

Malheureusement, la boucle \'while\' du \'main\' n\'est pas synchronisée avec la fréquence d\'affichage de l\'écran. Il exécute aussi vite que possible l\'incrémentation des coordonnées de la forme à l\'endroit où elle doit être affichée. Il doit le faire environ 50 fois avant que la routine \'on_irq\' soit appelée. En conséquence, la routine d\'affichage efface la forme se trouvent en \'old_x\' et \'old_y\' qui a été mise à jour 50 fois depuis le dernier affichage. Donc, l\'endroit effacé n\'est pas le bon.

Faire que cela fonctionne

Nous pouvons résoudre le problème en disant à la boucle \'while\' de \'se mettre en pause\' jusqu\'à ce que l\'interruption arrive. L\'effet qui s\'en suit est que notre boucle while sera moins \'occupée\'. Le processeur ARM9 peut en effet ralentir jusqu\'à ce que l\'interruption arrive. Ce permet aussi à notre framerate de s\'approcher des 60 trames par seconde sans aucun effort. La fonction utilisée pour réaliser cette attente se nomme \'swiWaitForVBlank\'.

Vous devez vous souvenir que dans la fonction \'on_irq\' nous avons mis à jour des registres pour indiquer que nous avons réalisé l\'interruption VBLANK. C\'est obligatoire pour que \'swiWaitForVBlank\' fonctionne. Si nous ne mettons pas à jour ces registres, la fonction \'swiWaitForVBlank\' va attendre indéfiniment que le hardware lui indique que l\'interruption est gérée....ce qui n\'arrivera jamais.

En ajoutant cette simple ligne, notre programme exemple fonctionne sur harware et sur Dualis correctement :

int main(void)
{
    powerON(POWER_ALL);
    videoSetMode(MODE_FB0);
    vramSetBankA(VRAM_A_LCD);
    InitInterruptHandler();
    while(1)
    {
        old_x = shape_x;
        old_y = shape_y;
        shape_x++;
        if(shape_x + shape_width >= SCREEN_WIDTH)
        {
            shape_x = 0;
            shape_y += shape_height;
            if(shape_y + shape_height >= SCREEN_HEIGHT)
            {
                shape_y = 0;
            }
        }
        swiWaitForVBlank();
    }

    return 0;
}

Vous trouverez ici l\'exécution dans cette capture d\'écran de Dualis.

Inverser les écrans

Dans certaines versions de Dualis, les écran du haut et du bas apparaissent inversés par rapport à la version sur hardware. Dans ce cas, Dualis affecte l\'écran principal par défaut à l\'écran du bas.

J\'ai déjà mentionné comme faire pour que l\'écran principal soit redirigé vers l\'écran du bas ou du haut sur harware. La démo actuelle s\'exécute avec l\'écran principal redirigé vers l\'écran du haut. On peut inverser les 2 écrans en utilisant la fonction \'lcdSwap\'. En appelant cette fonction, on définie l\'écran principal comme étant l\'écran tactile du bas. Cette fonction peut être appelée à n\'importe quel moment pour inverser les 2 écrans. Le main modifié ci-dessous va afficher l\'application sur l\'écran tactile :

int main(void)
{
    powerON(POWER_ALL);
    videoSetMode(MODE_FB0);
    vramSetBankA(VRAM_A_LCD);
    InitInterruptHandler();
    lcdSwap();
    while(1)
    {
        old_x = shape_x;
        old_y = shape_y;
        shape_x++;
        if(shape_x + shape_width >= SCREEN_WIDTH)
        {
            shape_x = 0;
            shape_y += shape_height;
            if(shape_y + shape_height >= SCREEN_HEIGHT)
            {
                shape_y = 0;
            }
        }
        swiWaitForVBlank();
    }

    return 0;
}

Création de l\'exécutable pour la DS

Les étapes pour construire l\'application sont exactement les mêmes que celle décrites dans le premier tutorial. J\'utilise par défaut le code pour l\'ARM7 dans un fichier arm7_main.cpp et celui de l\'ARM9 dans arm9_main.cpp. Un fichier Makefile est disponible pour exécuter les commandes du compilateur.

Le code source complet se trouve dans framebuffer_demo1.zip et vous pouvez télécharger les fichiers framebuffer_demo1.nds et framebuffer_demo1.nds.gba pour l\'exécution sur émulateur et hardware.

Conclusion

En utilisant la technique du framebuffer de ce tutorial, vous pouvez mettre en place des animations simples et des jeux. En utilisant le code générique de gestion de l\'écran tactile du Tutorial 1, vous pouvez même déplacer les objets sur l\'écran. Un prochain tutorial ira plus dans le détail de ces fonctionnalités. D\'autres tutoriaux parlerons du double buffering, du son et d\'autres encore des différents modes d\'affichages. screen modes.

Les mises à jour de ce tutorial peuvent être obtenues sur le weblog de l\'auteur (Chris Double) : http://radio.weblogs.com/0102385. Il peut être contacté à l\'adresse chris.double@double.co.nz

Copyright (c) 2005, Chris Double. Tous droits réservés. Traduction par AlekMaul (alekmaul@portabledev.com).