Programmation graphique portable en C avec la SDL - Partie 1: les bases

Anomaly

Ce premier article présente la création d'une application graphique SDL minimale, ainsi que la fonction de base du graphisme : afficher un pixel.

- Titre : Programmation graphique portable en C avec la SDL - Partie 1: les bases

- Auteur : Anomaly

- Parution : 16 novembre 2005

- Mise à jour : 25 mars 2015

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Anomaly. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Introduction

- 2. Installer la bibliothèque SDL

- 2-1. Linux & Unix

- 2-2. Windows

- 3. Notre premier programme SDL

- 3-1. Code source complet

- 3-2. Analyse des fonctions d'initialisation

- 3-3. Les surfaces

- 3-3-1. Les surfaces d'affichage logicielles

- 3-3-2. Les surfaces d'affichage matérielles

- 3-4. La gestion des événements

- 4. Afficher un pixel

- 4-1. La fonction setPixel()

- 4-2. Les couleurs

- 4-3. Programme "Ciel étoilé"

- 5. Conclusion

1 - Introduction

La SDL, Simple DirectMedia Layer, est une bibliothèque destinée à permettre l'accès au matériel graphique pour faire, par exemple, des jeux en plein écran (ou en fenêtre), et ça de manière portable. La SDL peut notamment fonctionner sous Linux (où elle utilise X11), comme sous Windows (où elle utilise DirectX).

La SDL est simple d'emploi, mais particulièrement bas niveau. Vous pouvez initialiser facilement le mode graphique, mais pour dessiner, vous n'avez rien. Même pas une fonction pour afficher un pixel ! Cet article est là pour vous aider à utiliser cette bibliothèque et à écrire les fonctions de base.

La SDL est disponible en licence libre LGPL. Cela veut dire que vous pouvez donc librement utiliser cette bibliothèque dans votre application, qu'elle soit libre ou commerciale. La seule obligation est que la bibliothèque SDL doit rester en liaison dynamique.

2 - Installer la bibliothèque SDL

Je vous présente ici la manière d'installer SDL sur votre système d'exploitation ainsi que la manière dont vous devrez compiler vos applications.

2-1 - Linux & Unix

Heureux possesseurs d'un système Unix, vous apprendrez avec plaisir que c'est un bonheur de travailler avec la SDL sous cet environnement.

Vous devez disposer simplement du compilateur gcc, ainsi que les paquets SDL et développement SDL (sdl-devel pour la majorité des distributions Linux). La SDL est livrée en standard pour la quasi-totalité des distributions Linux.

Pour compiler votre programme, vous ajouterez `sdl-config --cflags` aux options de compilation (attention, ce sont réellement des anti-quotes). Pour lier votre programme, vous ajouterez `sdl-config --libs` aux options de liaison.

Exemple : compiler un fichier source et le lier

gcc `sdl-config --cflags` -Wall -c TestSDL01.c
gcc `sdl-config --libs` -o TestSDL01 TestSDL01.o

Techniquement, sdl-config est un petit programme qui affiche à l'écran les options de gcc nécessaires pour la compilation ou la liaison selon les paramètres. La mise entre anti-quotes fait que cet affichage est récupéré et utilisé directement comme paramètres de gcc. Ce mécanisme, appelé substitution de commande, est géré directement par le shell.

2-2 - Windows

Vous pouvez télécharger la SDL ici. Il y a la version de développement, nécessaire pour compiler votre programme. Il en existe deux versions : une pour MingW (gcc ; cela concerne aussi DevC++), et une pour Visual C++ (version 5 et supérieures). La version de développement inclut aussi le runtime, vous n'avez donc pas besoin de télécharger ce dernier.

Nous allons détailler l'installation de la bibliothèque SDL sous Dev-C++.

Installer SDL sous Dev-C++

1. Téléchargez (lien donné ci-dessus) le fichier SDL-devel-1.2.9-mingw32.tar.gz

2. Décompressez cette archive dans le dossier de DevC++ (par défaut: C:\Dev-Cpp)

3. Lancez DevC++, allez dans "Outils", "Options du Compilateur", onglet "Répertoire"

4. Pour "Répertoire bibliothèques", ajoutez le chemin des fichiers .lib de SDL (par défaut: C:\Dev-Cpp\SDL-1.2.9\lib).

5. Pour "Répertoire C .h", ajoutez le chemin des fichiers .h de SDL (par défaut: C:\Dev-Cpp\SDL-1.2.9\include).

6. Pour "Répertoire C++ .h", ajoutez la même chose qu'à l'étape précédente (par défaut: C:\Dev-Cpp\SDL-1.2.9\include).

7. Validez le tout en cliquant sur OK : c'est installé.

Créer un projet SDL sous Dev-C++

1. Créez normalement un projet vide ("Fichier", "Nouveau", "Projet", "Empty Project"), donnez-lui un nom et sauvegardez-le dans son propre répertoire.

2. Allez dans les options du projet ("Projet", "Options du Projet")

3. Dans l'onglet "Général", choisissez "Win32 GUI".

4. Dans l'onglet "Paramètres", zone "Editeur de liens", ajoutez "-lmingw32 -lSDLmain -lSDL"

5. Validez le tout en cliquant sur OK.

6. Copiez le runtime SDL (SDL.DLL, que l'on trouve dans le dossier bin de la SDL, par défaut: C:\Dev-Cpp\SDL-1.2.9\bin) dans le dossier de votre projet.

7. C'est prêt !

3 - Notre premier programme SDL

Pour commencer, nous allons créer un programme minimal qui initialise une fenêtre, et qui attend une touche pour quitter.

3-1 - Code source complet

Voici le code source complet de ce premier programme, nous l'analyserons après :

#include <stdlib.h>
#include <stdio.h>
#include "SDL.h"

SDL_Surface* affichage;

void initSDL(void);
void attendreTouche(void);

int main(int argc, char** argv)
{
initSDL();
attendreTouche();
return EXIT_SUCCESS;
}

void initSDL(void)
{
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "Erreur à l'initialisation de la SDL : %s\n", SDL_GetError());
exit(EXIT_FAILURE);
}

atexit(SDL_Quit);
affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

if (affichage == NULL) {
fprintf(stderr, "Impossible d'activer le mode graphique : %s\n", SDL_GetError());
exit(EXIT_FAILURE);
}

SDL_WM_SetCaption("Mon premier programme SDL", NULL);
}

void attendreTouche(void)
{
SDL_Event event;

do
SDL_WaitEvent(&event);
while (event.type != SDL_QUIT && event.type != SDL_KEYDOWN);
}

Compilez ce programme et exécutez-le. Il affiche une fenêtre noire, de taille 800x600. Si vous enfoncez une touche, la fenêtre disparaît et le programme s'arrête. Remarquez que si vous cliquez sur la case de fermeture, il se passe la même chose.

3-2 - Analyse des fonctions d'initialisation

Maintenant, examinons le programme ligne par ligne.

#include "SDL.h"

Ceci inclut tous les fichiers headers de la SDL.

SDL_Surface* affichage;

Cette variable globale sert à stocker la surface concernant l'affichage. Nous détaillerons un peu plus loin ce que représente une surface, mais pour le moment, il suffit de savoir que lorsque nous voudrons dessiner directement à l'écran, nous utiliserons cette variable.

SDL_Init(SDL_INIT_VIDEO);

Cette fonction initialise la bibliothèque SDL. Elle doit être appelée avant toute autre fonction de la bibliothèque. Le paramètre correspond aux sous-systèmes qu'il faut activer. En effet, il faut savoir que la SDL est constituée de huit sous-systèmes : Audio, CD-ROM, Gestion des événements, Gestion de fichiers, Gestion du joystick, Processus légers (threads), Timers (fonctions de délai ultra-précises) et Vidéo.

SDL_Init() active automatiquement les sous-systèmes Gestion des événements, Gestion de fichiers et les Processus légers ; si on a besoin d'autres sous-systèmes, on le précise en paramètre de cette fonction. Ici, comme on va faire du graphisme, on initialise en plus le sous-système Vidéo.

atexit(SDL_Quit);

Quand l'application a initialisé la SDL avec SDL_Init(), elle ne doit surtout pas oublier de "l'éteindre" avant que le programme se termine, afin que les sous-systèmes soient correctement fermés : notamment, si vous avez changé le mode vidéo, il faut restaurer le mode normal.

Pour "éteindre" la SDL, il faut appeler la fonction SDL_Quit(). Mais il est encore mieux de s'assurer que cette fonction sera appelée automatiquement quand le programme s'arrête, quelle qu'en soit la raison. C'est la raison d'être de la fonction atexit() (de la bibliothèque C standard), qui enregistre les fonctions qu'il faudra appeler automatiquement à la fin du programme.

Nota: Sous Unix, si le programme est tué par un signal SIGKILL (avec kill -9 par exemple), la fonction SDL_Quit() ne sera malheureusement pas appelée.

affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

Cette fonction active le mode vidéo demandé. Remarquez que ceci ne signifie pas forcément changer de mode graphique : on peut très bien seulement créer une fenêtre dans laquelle on travaillera.

On lui passe les dimensions (je suppose ici que vous travaillez en 1024x768 afin que la fenêtre puisse s'afficher entièrement), ainsi que le nombre de bits par pixel (32). Remarquez, dans ce cas précis, qu'il n'est pas nécessaire que ce nombre de bits par pixel soit identique à la véritable résolution de l'écran. Si cela ne correspond pas, la SDL se chargera de convertir les accès quand ça sera nécessaire. Nous avons choisi 32 bits par pixel parce qu'il s'agit de la résolution la plus courante.

Le dernier argument indique le type de surface (nous y reviendrons). Ici, nous avons demandé une surface logicielle, sans demander le plein écran : SDL_SetVideoMode() va donc créer une fenêtre. Si nous voulions que l'application tourne en plein écran, nous aurions mis : SDL_SWSURFACE | SDL_FULLSCREEN.

SDL_WM_SetCaption("Mon premier programme SDL", NULL);

Sans surprise, cette fonction permet de donner un titre à cette fenêtre. Le premier argument est une chaîne contenant le titre, le deuxième argument une chaîne contenant l'icône à attribuer.

3-3 - Les surfaces

Qu'est-ce qu'une surface ? Hé bien c'est une zone de mémoire qui contient des pixels. Une surface, ça peut très bien être une image bitmap chargée en mémoire centrale, ou une zone directement en mémoire vidéo.

Quand on initialise le mode vidéo avec SetVideoMode(), on obtient en retour une surface. Cette surface permet de dessiner à l'intérieur de la fenêtre (ou sur l'écran si vous avez choisi le mode plein écran).

Les surfaces destinées à l'affichage peuvent être de deux types : logicielle, ou matérielle.

3-3-1 - Les surfaces d'affichage logicielles

Les surfaces logicielles sont stockées en mémoire centrale. On peut alors librement accéder à cette surface pour y écrire des pixels ou ce que l'on souhaite. Comme cette zone est en mémoire centrale, l'accès est très rapide, mais ce que l'on dessine n'apparaît pas à l'écran. En effet, lorsque le moment opportun sera arrivé, on pourra alors demander à SDL de mettre à jour l'affichage ; c'est-à-dire de copier le contenu de la surface en mémoire vidéo. Cette opération n'est pas très rapide, et peut même s'avérer très lente si le format de la surface n'est pas le même que la résolution réelle (bits par pixel) de l'écran.

Ceci dit, il n'est pas nécessaire de mettre à jour tout l'écran systématiquement. Si on n'a modifié que quelques portions, il suffit de demander la mise à jour du plus petit rectangle contenant ces modifications avec SDL_UpdateRect() ou SDL_UpdateRects() s'il s'agit de plusieurs rectangles, ce qui accélère fortement la vitesse de mise à jour. Cependant, s'il y a de très nombreux rectangles à mettre à jour, il reste plus simple et plus rapide de demander la mise à jour de l'écran entier avec SDL_UpdateRect(affichage, 0, 0, 0, 0).

Pour résumer, les avantages :

- Accès très rapide

- N'importe quelle résolution (bits par pixel) peut être utilisée

- Double tampon : l'affichage n'est mis à jour que sur demande

- Fonctionne en mode fenêtré ou en plein écran

- Fonctionne sur tous les systèmes

Les inconvénients :

- Ne profite pas des fonctions accélérées de la carte graphique

- L'opération de mise à jour de l'affichage est lente

3-3-2 - Les surfaces d'affichage matérielles

Au contraire des surfaces logicielles, les surfaces matérielles sont stockées directement en mémoire vidéo. Ce qui signifie que, normalement, lorsqu'on écrit un pixel dans cette surface, l'affichage est mis à jour instantanément ! Si cela pose un problème, il suffit d'activer le double tampon matériel : on dispose alors de deux pages écran : la page affichée et la page cachée, qui se suivent dans la mémoire vidéo.

Lorsqu'on écrit dans la surface, on écrit dans la page cachée ; les modifications ne sont donc pas visibles parce que l'écran affiche toujours le contenu de la page affichée. Lorsqu'on désire mettre à jour l'affichage, il suffit d'appeler SDL_Flip() qui consiste à tout simplement inverser les deux pages, ce qui met à jour l'affichage. Cette opération est extrêmement rapide puisqu'il n'y a strictement aucune copie : elle ne fait que programmer quelques registres de la carte vidéo.

Remarquons que ce mode double tampon n'est pas équivalent au double tampon qu'offrent les surfaces logicielles : en effet, après une mise à jour de l'affichage d'une surface logicielle, la surface de dessin contient exactement ce que contient l'affichage. Sur un double tampon d'une surface matérielle, en revanche, après un SDL_Flip(), la surface de dessin contient le précédent contenu affiché.

Néanmoins, l'accès direct à la mémoire vidéo peut poser problème dans un environnement multitâche. Le programme ne peut pas faire ce qu'il veut ! C'est pour ça que, avant tout accès à la surface, il doit verrouiller la surface, ce qui interdit à toute autre application de tenter d'accéder à la mémoire vidéo. Une fois le travail de dessin terminé, il doit toujours déverrouiller la surface.

Pour résumer, les avantages :

- Peut fonctionner en affichage direct, sans nécessité de mise à jour

- Mode double tampon (optionnel) avec mise à jour ultrarapide

- Peut profiter des fonctions accélérées de la carte graphique

Les inconvénients :

- Ne fonctionne qu'en plein écran

- Ne fonctionne pas sur tous les systèmes (notamment il faut des droits sous Unix)

- Nécessite de contrôler strictement l'accès à la surface

Comme notre programme n'a pas besoin de grandes performances, nous avons choisi d'utiliser une surface logicielle, pour son universalité et sa facilité d'utilisation.

3-4 - La gestion des événements

Maintenant, voyons quelque chose d'un peu plus technique. Un programme SDL ressemble à un programme Win32 normal : il est piloté par les événements. Un événement, ça peut être un clic souris, une touche clavier enfoncée, une demande de fermeture de la fenêtre principale, etc. Les événements, lorsqu'ils sont reçus, sont placés dans une file d'attente, où ils seront lus par le programme au moment voulu.

Pour lire les événements, on utilise une boucle. Dans cette boucle, on appelle SDL_WaitEvent(). Cette fonction fait attendre le programme tant qu'il n'y a pas d'événement à lire (il s'agit d'une attente passive : le cpu n'est pas du tout utilisé, les autres applications ne sont donc absolument pas pénalisées).

Lorsqu'un événement arrive, la fonction le lit, le retire de la file d'attente et le retourne au programme (en remplissant la structure SDL_Event passée en paramètre).

Dans notre premier programme, je me contente de lire (et retirer de la file) les événements jusqu'à ce que je reçoive un événement SDL_QUIT (clic sur la case de fermeture) ou SDL_KEYDOWN (touche enfoncée).

Nota: Si on oublie de gérer l'événement SDL_QUIT, la case de fermeture de la fenêtre n'aura pas d'effet. Sous Unix, remarquez qu'on ne peut pas arrêter un programme SDL avec Ctrl-C à la console ; en revanche, Ctrl - \ fonctionne (et désactive correctement la SDL).

4 - Afficher un pixel

4-1 - La fonction setPixel()

Aussi bizarre que ça puisse paraître au premier abord, la SDL ne propose aucun moyen d'écrire un pixel. Il nous faut écrire la fonction nous-même.

Mais en y regardant de plus près, cela s'explique aisément : écrire un pixel dépend énormément de la résolution (en bits par pixel). Si on veut gérer tous les cas, on perd du temps en exécution. De plus, un appel de fonction peut être trop lent dans certains cas. Enfin, on peut vouloir ou ne pas vouloir que la fonction teste si ses paramètres sont corrects (encore du temps d'exécution perdu).

Nous allons écrire une fonction simple, conçue pour fonctionner dans le mode 32 bits par pixel uniquement. Cette fonction ne teste pas la validité des coordonnées pour gagner en temps d'exécution. Si vous utilisez gcc (ou le C++), il sera une très bonne idée d'en faire une fonction "inline" afin de l'accélérer.

void setPixel(int x, int y, Uint32 coul)
{
*((Uint32*)(affichage->pixels) + x + y * affichage->w) = coul;
}

Pour comprendre grossièrement comment cette fonction est conçue, il faut connaître le format d'affichage. Quand la SDL fonctionne en mode 32 bits, la surface d'affichage est toujours conçue de la même manière : il faut 4 octets par pixel (le code couleur), et les points sont répartis linéairement, ligne par ligne.

Donc pour afficher un pixel, on prend l'adresse de début de la surface ; on décale le pointeur vers la bonne position (y lignes de pixels, soit y multiplié par la largeur de la surface, plus x colonnes). Comme le pointeur est transtypé en pointeur vers entier 32 bits, le décalage se fait automatiquement par sauts de 4 octets. Enfin, on déréférence le pointeur et on y stocke la couleur.

4-2 - Les couleurs

Contrairement à l'affichage de pixels, la SDL fournit une fonction qui permet de faire la conversion d'un code couleur RVB en code couleur. L'avantage, c'est qu'on n'a pas besoin de se préoccuper du format actuel des pixels.

Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);

Le premier paramètre est le format des pixels, pour ça on devra lui passer affichage->format.

Nous ne sommes pas tous des experts en mélange de couleurs, alors j'ai préparé ici une fonction qui charge une liste des couleurs les plus utilisées.

enum {
C_NOIR, C_BLEU_FONCE, C_VERT_FONCE, C_CYAN_FONCE, C_ROUGE_FONCE,
C_MAGENTA_FONCE, C_OCRE, C_GRIS_CLAIR, C_GRIS, C_BLEU, C_VERT,
C_CYAN, C_ROUGE, C_MAGENTA, C_JAUNE, C_BLANC,

NB_COULEURS
};

Uint32 couleurs[NB_COULEURS];

void initCouleurs(void)
{
couleurs[C_NOIR]          = SDL_MapRGB(affichage->format, 0x00, 0x00, 0x00);
couleurs[C_BLEU_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x00, 0x80);
couleurs[C_VERT_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x80, 0x00);
couleurs[C_CYAN_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x80, 0x80);
couleurs[C_ROUGE_FONCE]   = SDL_MapRGB(affichage->format, 0x80, 0x00, 0x00);
couleurs[C_MAGENTA_FONCE] = SDL_MapRGB(affichage->format, 0x80, 0x00, 0x80);
couleurs[C_OCRE]          = SDL_MapRGB(affichage->format, 0x80, 0x80, 0x00);
couleurs[C_GRIS_CLAIR]    = SDL_MapRGB(affichage->format, 0xC0, 0xC0, 0xC0);
couleurs[C_GRIS]          = SDL_MapRGB(affichage->format, 0x80, 0x80, 0x80);
couleurs[C_BLEU]          = SDL_MapRGB(affichage->format, 0x00, 0x00, 0xFF);
couleurs[C_VERT]          = SDL_MapRGB(affichage->format, 0x00, 0xFF, 0x00);
couleurs[C_CYAN]          = SDL_MapRGB(affichage->format, 0x00, 0xFF, 0xFF);
couleurs[C_ROUGE]         = SDL_MapRGB(affichage->format, 0xFF, 0x00, 0x00);
couleurs[C_MAGENTA]       = SDL_MapRGB(affichage->format, 0xFF, 0x00, 0xFF);
couleurs[C_JAUNE]         = SDL_MapRGB(affichage->format, 0xFF, 0xFF, 0x00);
couleurs[C_BLANC]         = SDL_MapRGB(affichage->format, 0xFF, 0xFF, 0xFF);
}

Après avoir appelé initCouleurs() en début de programme (après initSDL() bien entendu) pour initialiser le tableau, si vous souhaitez par exemple utiliser du jaune, il vous suffira de faire couleurs[C_JAUNE].

Le jeu de couleurs n'est pas tout à fait choisi par hasard : il s'agit des 16 couleurs classiques que l'on obtient dans un mode texte ou graphique 16 couleurs sur PC.

4-3 - Programme "Ciel étoilé"

Pour mettre en pratique l'affichage de pixels et les couleurs, nous allons écrire un petit programme qui affiche un ciel étoilé aléatoire (en couleurs).

#include <stdlib.h>
#include <stdio.h>
#include "SDL.h"

SDL_Surface* affichage;

void initSDL(void);
void attendreTouche(void);
void setPixel(int x, int y, Uint32 coul);
void actualiser(void);
void dessinerEtoiles(void);

int main(int argc, char** argv)
{
initSDL();
dessinerEtoiles();
actualiser();
attendreTouche();
return EXIT_SUCCESS;
}

void initSDL(void)
{
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "Erreur à l'initialisation de la SDL : %s\n", SDL_GetError());
exit(EXIT_FAILURE);
}

atexit(SDL_Quit);
affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

if (affichage == NULL) {
fprintf(stderr, "Impossible d'activer le mode graphique : %s\n", SDL_GetError());
exit(EXIT_FAILURE);
}

SDL_WM_SetCaption("Ciel étoilé", NULL);
}

void attendreTouche(void)
{
SDL_Event event;

do
SDL_WaitEvent(&event);
while (event.type != SDL_QUIT && event.type != SDL_KEYDOWN);
}

void setPixel(int x, int y, Uint32 coul)
{
*((Uint32*)(affichage->pixels) + x + y * affichage->w) = coul;
}

void actualiser(void)
{
SDL_UpdateRect(affichage, 0, 0, 0, 0);
}

void dessinerEtoiles(void)
{
int i;

for (i = 0; i < 100; i++)
setPixel(rand() % 800, rand() % 600,
SDL_MapRGB(affichage->format,
rand() % 128 + 128, rand() % 128 + 128, rand() % 128 + 128));
}

5 - Conclusion

Pour résumer, nous avons présenté les bases de la programmation graphique avec la SDL. Vous savez désormais initialiser un mode graphique et écrire des pixels colorés.

Partie suivante