Pour le dernier opus de cette série d’articles d’initiation à SDL, nous allons aborder tout ce (threads, gestion du cédérom …) que nous n’avons pas pu classer dans l’un des articles précédents.
En effet, SDL dispose d’un ensemble de fonctionnalités simples visant à faciliter la tache du programmeur (suivant le paradigme de simplicité cher au Simple Directmedia Layer…). La présence de ces fonctions renforce la portabilité des programmes SDL, qui n’ont plus à se tourner vers les équivalents de chaque système. SDL dispose de fonctions utilitaires pour la programmation multithread (gestion des threads et verrous d’exclusion mutuelle), les timers, la gestion des lecteurs de CDROM, ainsi que diverses commodités pour gérer les différences entre architectures matérielles.
La plupart des programmes modernes reposent, d’une manière ou d’une autre, sur la capacité du système d’exploitation d’opérer en multitâches. Les ‘threads’ sont des unités d’exécutions parallèles, tout comme les autres processus du système, à cette différence qu’elles partagent un même espace d’adressage, ce qui facilite grandement la coopération.
SDL propose les mécanismes de base pour la multi-programmation. Ceux-ci reposent sur les threads natives du système (POSIX threads pour Linux et la plupart des Unix). Il est possible de créer de nouvelles threads, par l’intermédiaire des fonctions suivantes :
Cette fonction crée une nouvelle thread, dont le point d’entrée est la fonction passée en paramètre. La fonction constituant la thread peut prendre un argument optionnel, correspondant à l’argument data
de SDL_CreateThread()
. L’exécution de la thread commence immédiatement après l’appel de SDL_CreateThread()
. La structure SDL_Thread
est un type opaque utilisé pour manipuler la thread ainsi créée. Il est à noter qu’elle se termine en même temps que la fonction qui la compose…
Cette fonction permet de stopper l’exécution d’une thread. D’une manière générale, il vaut mieux éviter d’employer cette fonction qui provoque une fin brutale. Nous vous conseillons d’utiliser les mécanismes de synchronisation décrits ci-dessous, pour terminer proprement son exécution.
Cette fonction est bloquante, jusqu’à ce que la thread désignée se termine. Le code de retour est ensuite affecté à la variable pointée par status
. Ce code correspond à la valeur retournée par la fonction exécutée par la thread.
Cette fonction renvoie un entier positif identifiant la thread en cours d’exécution. SDL ne garantit rien quant à la valeur retournée (si elle correspond à un numéro de processus, par exemple). Par conséquence, son seul usage valide est la comparaison avec d’autres identifiants, par exemple, pour déterminer si vous êtes en train d’exécuter du code dans une thread particulière.
La programmation parallèle ne peut se faire sans les mécanismes de synchronisation de base. SDL fournit des verrous d’exclusion mutuelle (“mutex”), qui permettent principalement de délimiter des sections de code ne pouvant être accédées que par une seule thread à la fois. Fort heureusement, la plupart des autres mécanismes de synchronisation peuvent être implantés à partir de mutex. Reste l’espoir que les futures versions de SDL fournissent d’autres mécanismes plus avancés. L’utilisation des mutex est très simple:
Explicite, cette fonction crée un nouveau mutex, identifié par une variable de type SDL_mutex
.
Utilisez cette fonction pour libérer un mutex préalablement alloué via SDL_CreateMutex()
.
Cette fonction verrouille le mutex désigné. Si le mutex est déjà verrouillé, cette fonction se met en attente, jusqu’à ce que le mutex ne soit plus verrouillé. La fonction renvoie -1, en cas d’erreur et 0, si tout s’est bien passé.
Contrepartie de la fonction précédente, cette fonction déverrouille un mutex précédemment verrouillé par SDL_mutexP()
. Cette fonction est non-bloquante et renvoie également -1, en cas d’erreur.
Concrètement, le programmeur ‘encadre’ les sections de programme ne devant être utilisées que par une thread à la fois (par exemple, pour protéger l’accès à des données communes) par des appels à SDL_mutexP()
et SDL_mutexV()
, comme le montre le programme d’exemple de l’encadré 1.
SDL permet également de gérer le temps et de programmer des appels répétitifs à des functions (timers).
Cette fonction, très utile, renvoie le nombre de millisecondes écoulées depuis l’initialisation de SDL.
Cette fonction bloque le programme pendant ‘ms’ millisecondes.
SDL 1.0 permet la définition d’un simple timer, autrement dit une fonction appelée à intervalles réguliers. Pour pouvoir utiliser ceux-ci, SDL_Init()
doit avoir reçu en appel le flag SDL_INIT_TIMER. Deux implémentations des timers sont disponibles sur la plupart des systèmes supportés par SDL. Sous Linux et autres systèmes permettant une gestion des événements dans une thread séparée, les timers sont simulés de manière générique depuis cette thread. Dans le cas contraire, tous les systèmes disposent de fonctions natives pour les timers, par l’intermédiaire de setitimer()
sous Unix, par exemple. Spécialement sous Linux où les timers sont implantés par le biais de signaux, il est fortement recommandé d’utiliser les timers ‘threadés’, si le programme est lui-même multithreadé, cela étant dû aux problèmes d’interaction entre signaux et threads multiples.
Le prototype de la fonction doit correspondre au type suivant:
La fonction timer est appelée avec, comme argument, la valeur actuelle de l’intervalle d’appel. La fonction est chargée de retourner la valeur du prochain intervalle ou 0, si le timer doit s’arrêter.
La mise en route du timer se fait par l’appel de la fonction suivante :
La variable interval
est exprimée en millisecondes. Il ne peut y avoir qu’une seule fonction timer à la fois qui utilise cette fonction. L’arrêt du timer s’effectue par l’appel de SDL_SetTimer(0,0)
.
Si le programmeur a besoin de plusieurs timers simultanés, il est possible d’utiliser les nouvelles fonctions nouvellement introduites dans SDL 1.1.2, permettant la définition de timers multiples. Il est à noter que ces fonctions ne sont disponibles que si la gestion threadée des événements est disponible.
Le prototype des fonctions timer permet dorénavant de prendre un argument supplémentaire dont la signification est laissée à la charge de l’utilisateur.
Le programmeur peut définir autant de timers simultanés qu’il le désire, par l’intermédiaire de la nouvelle fonction SDL_AddTimer()
:
Son utilisation est globalement similaire à SDL_SetTimer()
, à la différence qu’elle retourne un identifiant pour le timer qui vient d’être défini, afin de pouvoir le référencer par la suite.
Pour arrêter un timer unique, il faut utiliser la fonction SDL_RemoveTimer()
définie comme suit et qui retourne une valeur booléenne indiquant si tout s’est bien déroulé.
En guise d’illustration, le programme ‘testtimer.c’ fourni avec SDL et reproduit dans l’encadré 2 démontre comment utiliser ces fonctions.
SDL permet de contrôler le ou les lecteur/s de CDROM présents et configurés sur le système (incluant aussi implicitement les lecteurs DVD-ROM). Cela concerne principalement la lecture de pistes audio.
Renvoie le nombre de lecteurs installés.
Renvoie le nom associé au lecteur numéro ‘drive’ (le premier lecteur est le numéro 0). Le nom est de la forme /dev/cdrom
sous Linux. Sous d’autres systèmes, le nom sera vraisemblablement différent (par exemple D:
sous Windows).
Cette fonction renvoie un pointeur permettant de manipuler un lecteur particulier. NULL
est renvoyé, si une erreur quelconque est survenue.
Le lecteur est libéré par l’appel à la fonction SDL_CDClose()
définie comme suit:
Cette fonction renvoie l’état actuel du lecteur qui peut prendre l’une des valeurs suivantes : CD_TRAYEMPTY (pas de disque), CD_STOPPED (lecture arrêtée), CD_PLAYING (lecture en cours), CD_PAUSED (mode pause). Il est recommandé d’utiliser la macro CD_INDRIVE()
pour déterminer si le CD/DVD est dans le lecteur.
Cette fonction permet d’éjecter un CD sur le lecteur désigné. La valeur retournée est négative, si une erreur est survenue, sinon, nulle.
Les fonctions suivantes permettent de jouer des pistes audio :
Leur utilisation est évidente d’après leurs noms. Toutes ces fonctions renvoient une valeur nulle, si tout s’est bien passé. La seule chose importante à savoir est que les unités utilisées pour désigner le segment audio à jouer (passé à SDL_CDPlay
), est en ‘frames’ audio. start
est la frame de départ et length est le nombre de frames à jouer. Une ‘frame’ audio correspond à 1/75ème de seconde pour les CD audio. La macro CD_FPS
est, par ailleurs, définie à la valeur 75.
Il y a principalement deux moyens d’obtenir l’adresse en frames, à partir des informations dont le programmeur dispose :
A partir d’un temps précis (heure, minutes, secondes). Dans ce cas, il faut utiliser la macro MSF_TO_FRAMES(m,s,f)
, qui renvoie l’index de la frame correspondant à la minute ‘m’, seconde ‘s’ plus ‘f’ frames additionnelles. Autrement dit, pour obtenir l’adresse 5 min. 30 sec. du CD audio, utiliser MSF_TO_FRAMES(5,30,0)
. Il existe également une macro FRAMES_TO_MSF()
qui permet d’obtenir les informations de durée, à partir d’un index de frame.
A partir des informations de pistes fournies par SDL dans la structure SDL_CD
:
Les informations des pistes sont décrites à l’aide de la structure suivante :
Ainsi, il est très facile d’obtenir les informations nécessaires pour accéder à une piste précise à partir de SDL_CD, par exemple :
Ce bout de programme jouera la piste audio numéro 3 du disque. Bien entendu, il faudrait normalement vérifier que cette piste existe auparavant…
Cette fonction est d’un usage plus simple que SDL_CDPlay()
. Cette fonction prend en argument un numéro de piste de début, le numéro de la frame à l’intérieur de ladite piste, le nombre de pistes à jouer et un éventuel nombre de frames supplémentaires. Notez que cette fonction évitera automatiquement de jouer les pistes de données non-audio.
Pour faciliter l’écriture de programmes portables entre différentes architectures, SDL propose des mécanismes permettant d’obtenir des informations sur l’architecture utilisée. L’information la plus importante est l’ordre des octets (“endianness”).
Sur les systèmes Intel et Alpha, les octets de poids faibles sont stockés en premier en mémoire (“little endian”), tandis que d’autres architectures (PowerPC, Sparc), stockent les octets de poids forts en premier (“big endian”). Cette différence est cruciale lorsque des données sont partagées entre architectures, que ce soit par réseau ou par fichiers et il faut veiller à effectuer la conversion appropriée.
Le fichier d’en-tête SDL_endian.h
définit la macro SDL_BYTEORDER, qui peut prendre la valeur soit SDL_LIL_ENDIAN ou SDL_BIG_ENDIAN, suivant l’ordre des octets du système sur lequel l’application est compilée. Pour effectuer la conversion proprement dite, les fonctions SDL_Swap16()
, SDL_Swap32()
et SDL_Swap64()
permettent de passer d’un ordre à l’autre.
Voilà, cette série d’articles touche à sa fin. Cependant, nous n’avons fait qu’effleurer les possibilités de cette formidable bibliothèque. Nous n’avons que brièvement évoqué les nouveautés introduites dans SDL 1.1, comme la gestion des joysticks ou le support d’OpenGL… Et de nouvelles bibliothèques voient encore le jour comme, par exemple, OpenAL pour le son 3D… C’est pour cette raison que nous vous donnons rendez-vous pour une nouvelle série d’articles qui aborderont des aspects plus avancés de la programmation de jeux sous Linux !
Stéphane Peter