Programmer un émulateur

Chapitre 6 : l'interface homme-machine

BestCoder

Maintenant que notre émulateur donne des résultats palpables, nous allons nous occuper des entrées utilisateur et du son.

Commentez

- Titre : Programmer un émulateur

- Auteur : BestCoder

- Parution : 15 février 2014

- Licence : Le contenu de cet article est rédigé par BestCoder et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Navigation

- II. Introduction

- III. Les entrées utilisateur

- III-A. Traitement des opcodes

- III-B. Testons le tout

- IV. Le son

- V. À vous de jouer !

- V-A. Améliorations

- V-B. La compatibilité

- V-C. À suivre

- V-C-1. Le son

- V-C-2. Les cycles

- V-C-3. Débogueur

- V-C-4. Optimisation

- V-D. Plus loin

- Navigation

Navigation

Tutoriel précédent : exemples avec quelques instructions

Sommaire

II - Introduction

Maintenant que notre émulateur donne des résultats palpables, nous allons nous occuper des entrées utilisateur et du son.

III - Les entrées utilisateur

L'entrée est faite avec un clavier qui possède 16 touches allant de 0 à F. Les touches « 8 », « 4 », « 6 » et « 2 » sont généralement utilisées pour l'entrée directionnelle.

Pour simuler ce clavier, nous allons utiliser le pavé numérique et la touche de direction droite. Ce choix est purement subjectif. Vous pourrez le modifier à votre guise si vous avez bien compris.

Il nous faudra une variable pour connaître l'état des boutons (pressés ou non). Pour ma part, j'ai utilisé un tableau de 16 Uint8 pour nos 16 boutons.

Et pour gérer les changements d'état de nos touches (pressé ou relâché), nous allons utiliser les événements « KEYDOWN » et « KEYUP » de SDL. On aura quelque chose qui ressemblera à ceci (les variables touches seront insérées dans la structure CPU).

Uint8 listen()
{

Uint8 continuer=1;
while( SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT: {continuer = 0;break;}
case SDL_KEYDOWN:{
switch(event.key.keysym.sym)
{
case SDLK_KP0:{ cpu.touche[0x0]=1;break;}
case SDLK_KP7:{ cpu.touche[0x1]=1;break;}
case SDLK_KP8:{ cpu.touche[0x2]=1;break;}
/*
Et toutes les autres touches
*/
default:{ break;}
} ;break;}
case SDL_KEYUP:{
switch(event.key.keysym.sym)
{
case SDLK_KP0:{ cpu.touche[0x0]=0;break;}
case SDLK_KP7:{ cpu.touche[0x1]=0;break;}
case SDLK_KP8:{ cpu.touche[0x2]=0;break;}
/*
Et toutes les autres touches
*/
default:{ break;}
} ;break;}

default:{ break;}
}

}

return continuer;
}

J'ai adopté la convention suivante : « 0 » signifie que la touche est relâchée, « 1 » que la touche est pressée.

III-A - Traitement des opcodes

On peut maintenant revenir sur tous les opcodes qui traitent les entrées utilisateur.

EXA1

Saute l'instruction suivante si la clé stockée dans VX n'est pas pressée.

EX9E

Saute l'instruction suivante si la clé stockée dans VX est pressée.

Ces deux instructions sont relativement simples. La variable VX contiendra un nombre variant de 0 à 15 (pour nos 16 touches). Il faudra vérifier la valeur de touche[VX] et agir en conséquence. Comme nous l'avons déjà vu précédemment, pour sauter une instruction, il suffit d'incrémenter pc de 2.

//EX9E saute l'instruction suivante si la clé stockée dans VX est pressée.
if(cpu.touche[V[b3]]==1) //1 = pressé ; 0 = relâché
{
cpu.pc+=2;
}

//EXA1 saute l'instruction suivante si la clé stockée dans VX n'est pas pressée.
if(cpu.touche[V[b3]]==0) //1 = pressé ; 0 = relâché
{
cpu.pc+=2;
}

Et pour finir avec le clavier, on a :

FX0A

L'appui sur une touche est attendu, puis la valeur correspondante est stockée dans VX.

Pour attendre l'appui sur une touche, on peut utiliser la fonction SDL_WaitEvent. Cette fonction s'exécute tant qu'aucune touche du jeu n'est appuyée. Si une touche quelconque est pressée, on inscrit sa valeur dans VX.

Voici le code correspondant :

Uint8 attendAppui(Uint8 b3)
{
Uint8 attend=1,continuer=1;

while(attend)
{
SDL_WaitEvent(&event);

switch(event.type)
{
case SDL_QUIT:{ continuer=0; attend=0; break;}

case SDL_KEYDOWN:{

switch(event.key.keysym.sym)
{

case SDLK_KP0:{ cpu.V[b3]=0x0;cpu.touche[0x0]=1;attend=0;break;}
case SDLK_KP7:{ cpu.V[b3]=0x1;cpu.touche[0x1]=1;attend=0;break;}
case SDLK_KP8:{ cpu.V[b3]=0x2;cpu.touche[0x2]=1;attend=0;break;}
/* Et le reste des touches */
default:{ break;}
} break;}

default:{ break;}
}
}

return continuer;
}

J'ai inséré quelques attributs supplémentaires, car je ne voulais pas que l'utilisateur n'ait pas la possibilité de quitter l'émulateur lorsque la fonction est en cours d'exécution. Mais si vous avez compris le principe, le code ne devrait pas poser de problème.

III-B - Testons le tout

Vous pouvez maintenant lancer un jeu comme « Breakout », par exemple. Si tout se passe bien, vous devriez obtenir quelque chose ressemblant à ceci :

IV - Le son

Cette minuterie est utilisée pour les effets sonores. Lorsque sa valeur est différente de zéro, un signal sonore est émis.

Le système sonore de la Chip 8 est relativement simple. Nous avions déjà déclaré notre minuterie sonore ; il suffit juste de poser une condition sur la variable compteurSon et de jouer un bip sonore, de préférence si sa valeur est différente de zéro. Je ne vais pas détailler les méthodes pour charger un son. Personnellement, j'utilise SDL_Mixer.

if(cpu.compteurSon!=0)
{
Mix_PlayChannel(0, son, 0); //permet de jouer le bip sonore
cpu.compteurSon=0;
}
//Rien de plus simple ^_^

Pour ceux qui utilisent des bibliothèques haut niveau, il arrivera un moment où émuler le son deviendra impossible. N'hésitez donc pas à faire des recherches sur le son numérique en général pour vos futurs émulateurs.

Si toutes les caractéristiques essentielles ont été implémentées, il ne reste plus qu'à mettre en place l'interface homme-machine et apporter d'éventuelles améliorations à notre émulateur.

V - À vous de jouer !

V-A - Améliorations

Pour finir notre émulateur, j'ai rajouté quelques fonctionnalités, à savoir :

- faire une pause ;

- redémarrer un jeu.

Je ne détaillerai pas les actions effectuées, mais vous pouvez ajouter plein d'autres choses à votre émulateur : créer un système de sauvegarde, faire un écran de taille réglable, élaborer une interface pour faciliter le lancement des jeux, etc.

Voici le code final de notre projet.

pixel.h

#ifndef PIXEL_H
#define PIXEL_H
#include <SDL/SDL.h>

#define NOIR  0
#define BLANC 1
#define l 64
#define L 32
#define DIMPIXEL 8
#define WIDTH   l*DIMPIXEL
#define HEIGHT  L*DIMPIXEL

typedef struct
{
SDL_Rect position; //regroupe l'abscisse et l'ordonnée
Uint32 couleur;   //comme son nom l'indique, c'est la couleur
} PIXEL;

SDL_Surface *ecran,*carre[2];
PIXEL pixel[l][L];
SDL_Event event;

void initialiserEcran();
void initialiserPixel();
void dessinerPixel(PIXEL pixel);
void effacerEcran();
void updateEcran();

#endif

pixel.c

#include "pixel.h"

void initialiserPixel()
{

Uint8 x=0,y=0;

for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
pixel[x][y].position.x=x*DIMPIXEL;
pixel[x][y].position.y=y*DIMPIXEL;
pixel[x][y].couleur=NOIR;
}
}

}

void initialiserEcran()
{
ecran=NULL;
carre[0]=NULL;
carre[1]=NULL;

ecran=SDL_SetVideoMode(WIDTH,HEIGHT,32,SDL_HWSURFACE);
SDL_WM_SetCaption("BC-Chip8 By BestCoder",NULL);

if(ecran==NULL)
{
fprintf(stderr,"Erreur lors du chargement du mode vidéo %s",SDL_GetError());
exit(EXIT_FAILURE);
}

carre[0]=SDL_CreateRGBSurface(SDL_HWSURFACE,DIMPIXEL,DIMPIXEL,32,0,0,0,0); //le pixel noir

if(carre[0]==NULL)
{
fprintf(stderr,"Erreur lors du chargement de la surface %s",SDL_GetError());
exit(EXIT_FAILURE);
}

SDL_FillRect(carre[0],NULL,SDL_MapRGB(carre[0]->format,0x00,0x00,0x00)); //le pixel noir

carre[1]=SDL_CreateRGBSurface(SDL_HWSURFACE,DIMPIXEL,DIMPIXEL,32,0,0,0,0); //le pixel blanc

if(carre[1]==NULL)
{
fprintf(stderr,"Erreur lors du chargement de la surface %s",SDL_GetError());
exit(EXIT_FAILURE);
}

SDL_FillRect(carre[1],NULL,SDL_MapRGB(carre[1]->format,0xFF,0xFF,0xFF));  //le pixel blanc
}

void dessinerPixel(PIXEL pixel)
{
/* pixel.couleur peut prendre deux valeurs : 0, auquel cas on dessine le pixel en noir, ou 1, on dessine alors le pixel en blanc */
SDL_BlitSurface(carre[pixel.couleur],NULL,ecran,&pixel.position);
}

void effacerEcran()
{
//Pour effacer l'écran, on remet tous les pixels en noir
Uint8 x=0,y=0;
for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
pixel[x][y].couleur=NOIR;
}
}

//on repeint l'écran en noir
SDL_FillRect(ecran,NULL,NOIR);
}

void updateEcran()
{
//On dessine tous les pixels à l'écran
Uint8 x=0,y=0;

for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
dessinerPixel(pixel[x][y]);
}
}

SDL_Flip(ecran); //on affiche les modifications
}

cpu.h

#ifndef CPU_H
#define CPU_H
#include "pixel.h"

#define TAILLEMEMOIRE 4096
#define ADRESSEDEBUT 512
#define NBROPCODE 35

typedef struct
{
Uint8 memoire[TAILLEMEMOIRE];
Uint8 V[16]; //le registre
Uint16 I; //stocke une adresse mémoire ou dessinateur
Uint16 saut[16]; //pour gérer les sauts dans memoire, 16 au maximum
Uint8 nbrsaut; //stocke le nombre de sauts effectués pour ne pas dépasser 16
Uint8 compteurJeu; //compteur pour le graphisme (fréquence de rafraîchissement)
Uint8 compteurSon; //compteur pour le son
Uint16 pc; //pour parcourir le tableau memoire
Uint8 touche[16]; //pour stocker l'état des touches
} CPU;

CPU cpu;

typedef struct
{
Uint16 masque[NBROPCODE];   //la Chip 8 peut effectuer 35 opérations, chaque opération possédant son masque
Uint16 id[NBROPCODE];   //idem, chaque opération possède son propre identifiant

}JUMP;

JUMP jp;

void initialiserJump();
void initialiserCpu();
void decompter();
void chargerFont();
void dessinerEcran(Uint8,Uint8,Uint8);
void reset();

Uint16 recupererOpcode();
Uint8 interpreterOpcode(Uint16);
Uint8 recupererAction(Uint16);
Uint8 attendAppui(Uint8);

#endif

cpu.c

#include "cpu.h"

void initialiserCpu()
{
//On initialise le tout
Uint16 i=0;

for(i=0;i<TAILLEMEMOIRE;i++)
{
cpu.memoire[i]=0;
}

for(i=0;i<16;i++)
{
cpu.V[i]=0;
cpu.saut[i]=0;
cpu.touche[i]=0;
}

cpu.pc=ADRESSEDEBUT;
cpu.nbrsaut=0;
cpu.compteurJeu=0;
cpu.compteurSon=0;
cpu.I=0;

initialiserJump();
}

void reset()
{
Uint8 i=0;
for(i=0;i<16;i++)
{
cpu.V[i]=0;
cpu.saut[i]=0;
cpu.touche[i]=0;
}

cpu.pc=ADRESSEDEBUT;
cpu.nbrsaut=0;
cpu.compteurJeu=0;
cpu.compteurSon=0;
cpu.I=0;
initialiserPixel();
updateEcran();

}

void initialiserJump()
{

jp.masque[0]= 0x0000; jp.id[0]=0x0FFF;          /* 0NNN */
jp.masque[1]= 0xFFFF; jp.id[1]=0x00E0;          /* 00E0 */
jp.masque[2]= 0xFFFF; jp.id[2]=0x00EE;          /* 00EE */
jp.masque[3]= 0xF000; jp.id[3]=0x1000;          /* 1NNN */
jp.masque[4]= 0xF000; jp.id[4]=0x2000;          /* 2NNN */
jp.masque[5]= 0xF000; jp.id[5]=0x3000;          /* 3XNN */
jp.masque[6]= 0xF000; jp.id[6]=0x4000;          /* 4XNN */
jp.masque[7]= 0xF00F; jp.id[7]=0x5000;          /* 5XY0 */
jp.masque[8]= 0xF000; jp.id[8]=0x6000;          /* 6XNN */
jp.masque[9]= 0xF000; jp.id[9]=0x7000;          /* 7XNN */
jp.masque[10]= 0xF00F; jp.id[10]=0x8000;          /* 8XY0 */
jp.masque[11]= 0xF00F; jp.id[11]=0x8001;          /* 8XY1 */
jp.masque[12]= 0xF00F; jp.id[12]=0x8002;          /* 8XY2 */
jp.masque[13]= 0xF00F; jp.id[13]=0x8003;          /* BXY3 */
jp.masque[14]= 0xF00F; jp.id[14]=0x8004;          /* 8XY4 */
jp.masque[15]= 0xF00F; jp.id[15]=0x8005;          /* 8XY5 */
jp.masque[16]= 0xF00F; jp.id[16]=0x8006;          /* 8XY6 */
jp.masque[17]= 0xF00F; jp.id[17]=0x8007;          /* 8XY7 */
jp.masque[18]= 0xF00F; jp.id[18]=0x800E;          /* 8XYE */
jp.masque[19]= 0xF00F; jp.id[19]=0x9000;          /* 9XY0 */
jp.masque[20]= 0xF000; jp.id[20]=0xA000;          /* ANNN */
jp.masque[21]= 0xF000; jp.id[21]=0xB000;          /* BNNN */
jp.masque[22]= 0xF000; jp.id[22]=0xC000;          /* CXNN */
jp.masque[23]= 0xF000; jp.id[23]=0xD000;          /* DXYN */
jp.masque[24]= 0xF0FF; jp.id[24]=0xE09E;          /* EX9E */
jp.masque[25]= 0xF0FF; jp.id[25]=0xE0A1;          /* EXA1 */
jp.masque[26]= 0xF0FF; jp.id[26]=0xF007;          /* FX07 */
jp.masque[27]= 0xF0FF; jp.id[27]=0xF00A;          /* FX0A */
jp.masque[28]= 0xF0FF; jp.id[28]=0xF015;          /* FX15 */
jp.masque[29]= 0xF0FF; jp.id[29]=0xF018;          /* FX18 */
jp.masque[30]= 0xF0FF; jp.id[30]=0xF01E;          /* FX1E */
jp.masque[31]= 0xF0FF; jp.id[31]=0xF029;          /* FX29 */
jp.masque[32]= 0xF0FF; jp.id[32]=0xF033;          /* FX33 */
jp.masque[33]= 0xF0FF; jp.id[33]=0xF055;          /* FX55 */
jp.masque[34]= 0xF0FF; jp.id[34]=0xF065;          /* FX65 */

}

Uint8 recupererAction(Uint16 opcode)
{
Uint8 action;
Uint16 resultat;

for(action=0; action<NBROPCODE;action++)
{
resultat= (jp.masque[action]&opcode);  /* On récupère les bits concernés par le test */

if(resultat == jp.id[action]) /* On a trouvé l'action à effectuer */
break; /* Plus la peine de continuer la boucle */
}

return action;
}

void decompter()
{
if(cpu.compteurJeu>0)
cpu.compteurJeu--;

if(cpu.compteurSon>0)
cpu.compteurSon--;
}

Uint16 recupererOpcode()
{
return (cpu.memoire[cpu.pc]<<8)+cpu.memoire[cpu.pc+1];
}

Uint8 interpreterOpcode(Uint16 opcode)
{
Uint8 continuer=1;
Uint8 b4,b3,b2,b1;

b3=(opcode&(0x0F00))>>8;  //on prend les 4 bits représentant X
b2=(opcode&(0x00F0))>>4;  //idem pour Y
b1=(opcode&(0x000F));     //idem

b4= recupererAction(opcode);

switch(b4)
{
case 0:{
//Cet opcode n'est pas implémenté.
break;
}
case 1:{
//00E0 efface l'écran.
effacerEcran();
break;
}

case 2:{
//00EE revient du saut.

if(cpu.nbrsaut>0)
{
cpu.nbrsaut--;
cpu.pc=cpu.saut[cpu.nbrsaut];
}
break;
}
case 3:{
//1NNN effectue un saut à l'adresse 1NNN.

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //on verra pourquoi à la fin

break;
}
case 4:{
//2NNN appelle le sous-programme en NNN, mais on revient ensuite.

cpu.saut[cpu.nbrsaut]=cpu.pc; //on reste là où on était

if(cpu.nbrsaut<15)
{
cpu.nbrsaut++;
}

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //on verra pourquoi à la fin

break;
}
case 5:{
//3XNN saute l'instruction suivante si VX est égal à NN.

if(cpu.V[b3]==((b2<<4)+b1))
{
cpu.pc+=2;
}

break;
}
case 6:{
//4XNN saute l'instruction suivante si VX et NN ne sont pas égaux.
if(cpu.V[b3]!=((b2<<4)+b1))
{
cpu.pc+=2;
}

break;
}
case 7:{
//5XY0 saute l'instruction suivante si VX et VY sont égaux.
if(cpu.V[b3]==cpu.V[b2])
{
cpu.pc+=2;
}

break;
}

case 8:{
//6XNN définit VX à NN.
cpu.V[b3]=(b2<<4)+b1;
break;
}
case 9:{
//7XNN ajoute NN à VX.
cpu.V[b3]+=(b2<<4)+b1;

break;
}
case 10:{
//8XY0 définit VX à la valeur de VY.
cpu.V[b3]=cpu.V[b2];

break;
}
case 11:{
//8XY1 définit VX à VX OR VY.
cpu.V[b3]=cpu.V[b3]|cpu.V[b2];

break;
}
case 12:{
//8XY2 définit VX à VX AND VY.
cpu.V[b3]=cpu.V[b3]&cpu.V[b2];

break;
}
case 13:{
//8XY3 définit VX à VX XOR VY.
cpu.V[b3]=cpu.V[b3]^cpu.V[b2];

break;
}
case 14:{
//8XY4 ajoute VY à VX. VF est mis à 1 quand il y a un dépassement de mémoire (carry), et à 0 quand il n'y en pas.
if((cpu.V[b3]+cpu.V[b2])>255)
{
cpu.V[0xF]=1; //cpu.V[15]
}
else
{
cpu.V[0xF]=0; //cpu.V[15]
}
cpu.V[b3]+=cpu.V[b2];

break;
}
case 15:{
//8XY5 VY est soustraite de VX. VF est mis à 0 quand il y a un emprunt, et à 1 quand il n'y a en pas.

if((cpu.V[b3]<cpu.V[b2]))
{
cpu.V[0xF]=0; //cpu.V[15]
}
else
{
cpu.V[0xF]=1; //cpu.V[15]
}
cpu.V[b3]-=cpu.V[b2];

break;
}
case 16:{
//8XY6 décale (shift) VX à droite de 1 bit. VF est fixé à la valeur du bit de poids faible de VX avant le décalage.
cpu.V[0xF]=(cpu.V[b3]&(0x01));
cpu.V[b3]=(cpu.V[b3]>>1);

break;
}
case 17:{
//8XY7 VX = VY - VX. VF est mis à 0 quand il y a un emprunt et à 1 quand il n'y en a pas.
if((cpu.V[b2]<cpu.V[b3]))
{
cpu.V[0xF]=0; //cpu.V[15]
}
else
{
cpu.V[0xF]=1; //cpu.V[15]
}
cpu.V[b3]=cpu.V[b2]-cpu.V[b3];

break;
}
case 18:{
//8XYE décale (shift) VX à gauche de 1 bit. VF est fixé à la valeur du bit de poids fort de VX avant le décalage.
cpu.V[0xF]=(cpu.V[b3]>>7);
cpu.V[b3]=(cpu.V[b3]<<1);

break;
}

case 19:{
//9XY0 saute l'instruction suivante si VX et VY ne sont pas égaux.
if(cpu.V[b3]!=cpu.V[b2])
{
cpu.pc+=2;
}

break;
}
case 20:{
//ANNN affecte NNN à I.

cpu.I=(b3<<8)+(b2<<4)+b1;

break;
}
case 21:{
//BNNN passe à l'adresse NNN + V0.

cpu.pc=(b3<<8)+(b2<<4)+b1+cpu.V[0];
cpu.pc-=2;

break;

}
case 22:{
//CXNN définit VX à un nombre aléatoire inférieur à NN.
cpu.V[b3]=(rand())%((b2<<4)+b1+1);

break;

}

case 23:{
//DXYN dessine un sprite aux coordonnées (VX, VY).

dessinerEcran(b1,b2,b3) ;

break;

}
case 24:{
//EX9E saute l'instruction suivante si la clé stockée dans VX est pressée.
if(cpu.touche[cpu.V[b3]]==1)//1 pressé, 0 relaché
{
cpu.pc+=2;
}

break;
}
case 25:{
//EXA1 saute l'instruction suivante si la clé stockée dans VX n'est pas pressée.
if(cpu.touche[cpu.V[b3]]==0)//1 pressé, 0 relaché
{
cpu.pc+=2;
}

break;
}

case 26:{
//FX07 définit VX à la valeur de la temporisation.
cpu.V[b3]=cpu.compteurJeu;

break;
}
case 27:{
//FX0A attend l'appui sur une touche et stocke ensuite la donnée dans VX.
continuer=attendAppui(b3);

break;
}

case 28:{
//FX15 définit la temporisation à VX.
cpu.compteurJeu=cpu.V[b3];

break;
}
case 29:{
//FX18 définit la minuterie sonore à VX.
cpu.compteurSon=cpu.V[b3];

break;
}
case 30:{
//FX1E ajoute VX à I. VF est mis à 1 quand il y a overflow (I+VX>0xFFF), et à 0 si tel n'est pas le cas.

if((cpu.I+cpu.V[b3])>0xFFF)
{
cpu.V[0xF]=1;
}
else
{
cpu.V[0xF]=0;
}
cpu.I+=cpu.V[b3];

break;
}

case 31:{
//FX29 définit I à l'emplacement du caractère stocké dans VX. Les caractères 0-F (en hexadécimal) sont représentés par une police 4x5.
cpu.I=cpu.V[b3]*5;

break;
}

case 32:{
//FX33 stocke dans la mémoire le code décimal représentant VX (dans I, I+1, I+2).

cpu.memoire[cpu.I]=(cpu.V[b3]-cpu.V[b3]%100)/100;
cpu.memoire[cpu.I+1]=(((cpu.V[b3]-cpu.V[b3]%10)/10)%10);
cpu.memoire[cpu.I+2]=cpu.V[b3]-cpu.memoire[cpu.I]*100-10*cpu.memoire[cpu.I+1];

break;
}
case 33:{
//FX55 stocke V0 à VX en mémoire à partir de l'adresse I.
Uint8 i=0;
for(i=0;i<=b3;i++)
{
cpu.memoire[cpu.I+i]=cpu.V[i];
}

break;
}
case 34:{
//FX65 remplit V0 à VX avec les valeurs de la mémoire à partir de l'adresse I.

Uint8 i=0;

for(i=0;i<=b3;i++)
{
cpu.V[i]=cpu.memoire[cpu.I+i];
}

break;
}

default: { //si ça arrive, il y un truc qui cloche

break;
}

}
cpu.pc+=2; //on passe au prochain opcode
return continuer;
}

void dessinerEcran(Uint8 b1,Uint8 b2, Uint8 b3)
{
Uint8 x=0,y=0,k=0,codage=0,j=0,decalage=0;
cpu.V[0xF]=0;

for(k=0;k<b1;k++)
{
codage=cpu.memoire[cpu.I+k]; //on récupère le codage de la ligne à dessiner

y=(cpu.V[b2]+k)%L; //on calcule l'ordonnée de la ligne à dessiner, on ne doit pas dépasser L

for(j=0,decalage=7;j<8;j++,decalage--)
{
x=(cpu.V[b3]+j)%l; //on calcule l'abscisse, on ne doit pas dépasser l

if(((codage)&(0x1<<decalage))!=0) //on récupère le bit correspondant
{   //si c'est blanc
if( pixel[x][y].couleur==BLANC) //le pixel était blanc
{
pixel[x][y].couleur=NOIR; //on l'éteint
cpu.V[0xF]=1; //il y a donc collusion
}
else //sinon
{
pixel[x][y].couleur=BLANC; //on l'allume
}

}
}
}

}

void chargerFont()
{
cpu.memoire[0]=0xF0;cpu.memoire[1]=0x90;cpu.memoire[2]=0x90;cpu.memoire[3]=0x90; cpu.memoire[4]=0xF0; //O

cpu.memoire[5]=0x20;cpu.memoire[6]=0x60;cpu.memoire[7]=0x20;cpu.memoire[8]=0x20;cpu.memoire[9]=0x70; //1

cpu.memoire[10]=0xF0;cpu.memoire[11]=0x10;cpu.memoire[12]=0xF0;cpu.memoire[13]=0x80; cpu.memoire[14]=0xF0; //2

cpu.memoire[15]=0xF0;cpu.memoire[16]=0x10;cpu.memoire[17]=0xF0;cpu.memoire[18]=0x10;cpu.memoire[19]=0xF0; //3

cpu.memoire[20]=0x90;cpu.memoire[21]=0x90;cpu.memoire[22]=0xF0;cpu.memoire[23]=0x10;cpu.memoire[24]=0x10; //4

cpu.memoire[25]=0xF0;cpu.memoire[26]=0x80;cpu.memoire[27]=0xF0;cpu.memoire[28]=0x10;cpu.memoire[29]=0xF0; //5

cpu.memoire[30]=0xF0;cpu.memoire[31]=0x80;cpu.memoire[32]=0xF0;cpu.memoire[33]=0x90;cpu.memoire[34]=0xF0; //6

cpu.memoire[35]=0xF0;cpu.memoire[36]=0x10;cpu.memoire[37]=0x20;cpu.memoire[38]=0x40;cpu.memoire[39]=0x40; //7

cpu.memoire[40]=0xF0;cpu.memoire[41]=0x90;cpu.memoire[42]=0xF0;cpu.memoire[43]=0x90;cpu.memoire[44]=0xF0; //8

cpu.memoire[45]=0xF0;cpu.memoire[46]=0x90;cpu.memoire[47]=0xF0;cpu.memoire[48]=0x10;cpu.memoire[49]=0xF0; //9

cpu.memoire[50]=0xF0;cpu.memoire[51]=0x90;cpu.memoire[52]=0xF0;cpu.memoire[53]=0x90;cpu.memoire[54]=0x90; //A

cpu.memoire[55]=0xE0;cpu.memoire[56]=0x90;cpu.memoire[57]=0xE0;cpu.memoire[58]=0x90;cpu.memoire[59]=0xE0; //B

cpu.memoire[60]=0xF0;cpu.memoire[61]=0x80;cpu.memoire[62]=0x80;cpu.memoire[63]=0x80;cpu.memoire[64]=0xF0; //C

cpu.memoire[65]=0xE0;cpu.memoire[66]=0x90;cpu.memoire[67]=0x90;cpu.memoire[68]=0x90;cpu.memoire[69]=0xE0; //D

cpu.memoire[70]=0xF0;cpu.memoire[71]=0x80;cpu.memoire[72]=0xF0;cpu.memoire[73]=0x80;cpu.memoire[74]=0xF0; //E

cpu.memoire[75]=0xF0;cpu.memoire[76]=0x80;cpu.memoire[77]=0xF0;cpu.memoire[78]=0x80;cpu.memoire[79]=0x80; //F

//OUF !

}

Uint8 attendAppui(Uint8 b3)
{
Uint8 attend=1,continuer=1;

while(attend)
{
SDL_WaitEvent(&event);

switch(event.type)
{
case SDL_QUIT:{ continuer=0;attend=0; break;}

case SDL_KEYDOWN:{

switch(event.key.keysym.sym)
{

case SDLK_KP0:{ cpu.V[b3]=0x0; cpu.touche[0x0]=1; attend=0;break;}
case SDLK_KP7:{ cpu.V[b3]=0x1; cpu.touche[0x1]=1; attend=0;break;}
case SDLK_KP8:{ cpu.V[b3]=0x2; cpu.touche[0x2]=1; attend=0;break;}
case SDLK_KP9:{ cpu.V[b3]=0x3; cpu.touche[0x3]=1; attend=0;break;}
case SDLK_KP4:{ cpu.V[b3]=0x4; cpu.touche[0x4]=1; attend=0;break;}
case SDLK_KP5:{ cpu.V[b3]=0x5; cpu.touche[0x5]=1; attend=0;break;}
case SDLK_KP6:{ cpu.V[b3]=0x6; cpu.touche[0x6]=1; attend=0;break;}
case SDLK_KP1:{ cpu.V[b3]=0x7; cpu.touche[0x7]=1; attend=0;break;}
case SDLK_KP2:{ cpu.V[b3]=0x8; cpu.touche[0x8]=1; attend=0;break;}
case SDLK_KP3:{ cpu.V[b3]=0x9; cpu.touche[0x9]=1; attend=0;break;}
case SDLK_RIGHT:{        cpu.V[b3]=0xA;       cpu.touche[0xA]=1;  attend=0;break;}
case SDLK_KP_PERIOD:{    cpu.V[b3]=0xB;     cpu.touche[0xB]=1;  attend=0;break;}
case SDLK_KP_MULTIPLY:{  cpu.V[b3]=0xC;     cpu.touche[0xC]=1;  attend=0;break;}
case SDLK_KP_MINUS:{     cpu.V[b3]=0xD;     cpu.touche[0xD]=1;  attend=0;break;}
case SDLK_KP_PLUS:{      cpu.V[b3]=0xE;     cpu.touche[0xE]=1;  attend=0;break;}
case SDLK_KP_ENTER:{     cpu.V[b3]=0xF;     cpu.touche[0xF]=1;  attend=0;break;}
default:{ break;}
} break;}

default:{ break;}
}
}

return continuer;
}

main.c

#include <SDL/SDL_mixer.h>
#include "cpu.h"

#define VITESSECPU 4 //nombre d'opérations par tour
#define FPS 16      //pour le rafraîchissement

void initialiserSDL();
void quitterSDL();
void pause();

Uint8 chargerJeu(char *);
Uint8 listen();

Mix_Chunk *son;

int main(int argc, char *argv[])
{
initialiserSDL();
initialiserEcran();
initialiserPixel();
initialiserCpu();
chargerFont();

Uint8 continuer=1,demarrer=0,compteur=0;

son=NULL;

son = Mix_LoadWAV("SON/beep.wav");

if(son==NULL)
{
fprintf(stderr,"Problème avec le son");
exit(EXIT_FAILURE);
}

if(argc>=2) //Permet de charger un jeu en ligne de commande ou en le plaçant dans l'exécutable
{
demarrer=chargerJeu(argv[1]);
}

if(demarrer==1)
{
do
{
continuer=listen(); //pour les entrées utilisateur

for(compteur=0;compteur<VITESSECPU && continuer==1;compteur++)  //Si continuer=0, on quitte l'émulateur
{
continuer=interpreterOpcode(recupererOpcode());
}

if(cpu.compteurSon!=0)
{
Mix_PlayChannel(0, son, 0);
cpu.compteurSon=0;
}

updateEcran();
decompter();

SDL_Delay(FPS); //une pause de 16 ms

}while(continuer==1);

}

return EXIT_SUCCESS;
}

void initialiserSDL()
{
atexit(quitterSDL);

if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)
{
fprintf(stderr,"Erreur lors de l'initialisation de la SDL %s",SDL_GetError());
exit(EXIT_FAILURE);
}

if(Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, 1024) == -1) //Initialisation de Mixer
{
fprintf(stderr,"Problème d'initialisation de SDL_MIXER: %s",Mix_GetError());
exit(EXIT_FAILURE);
}
Mix_AllocateChannels(1);

}

void quitterSDL()
{
SDL_FreeSurface(carre[0]);
SDL_FreeSurface(carre[1]);
Mix_FreeChunk(son);
Mix_CloseAudio();
SDL_Quit();
}

Uint8 chargerJeu(char *nomJeu)
{
FILE *jeu=NULL;
jeu=fopen(nomJeu,"rb");

if(jeu!=NULL)
{
fread(&cpu.memoire[ADRESSEDEBUT],sizeof(Uint8)*(TAILLEMEMOIRE-ADRESSEDEBUT), 1, jeu);
fclose(jeu);
return 1;
}
else
{
fprintf(stderr,"Problème d'ouverture du fichier");
return 0;
}

}

Uint8 listen()
{
Uint8 continuer=1;

while(SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT: {continuer = 0;break;}
case SDL_KEYDOWN:{
switch(event.key.keysym.sym)
{
case SDLK_KP0:{ cpu.touche[0x0]=1;break;}
case SDLK_KP7:{ cpu.touche[0x1]=1;break;}
case SDLK_KP8:{ cpu.touche[0x2]=1;break;}
case SDLK_KP9:{ cpu.touche[0x3]=1;break;}
case SDLK_KP4:{ cpu.touche[0x4]=1;break;}
case SDLK_KP5:{ cpu.touche[0x5]=1;break;}
case SDLK_KP6:{ cpu.touche[0x6]=1;break;}
case SDLK_KP1:{ cpu.touche[0x7]=1;break;}
case SDLK_KP2:{ cpu.touche[0x8]=1;break;}
case SDLK_KP3:{ cpu.touche[0x9]=1;break;}
case SDLK_RIGHT:{ cpu.touche[0xA]=1;break;}
case SDLK_KP_PERIOD:{cpu.touche[0xB]=1;break;}
case SDLK_KP_MULTIPLY:{cpu.touche[0xC]=1;break;}
case SDLK_KP_MINUS:{cpu.touche[0xD]=1;break;}
case SDLK_KP_PLUS:{cpu.touche[0xE]=1;break;}
case SDLK_KP_ENTER:{cpu.touche[0xF]=1;break;}
case SDLK_p:{pause();break;}
case SDLK_r:{reset();break;}
default:{ break;}
}
break;}
case SDL_KEYUP:{
switch(event.key.keysym.sym)
{
case SDLK_KP0:{ cpu.touche[0x0]=0;break;}
case SDLK_KP7:{ cpu.touche[0x1]=0;break;}
case SDLK_KP8:{ cpu.touche[0x2]=0;break;}
case SDLK_KP9:{ cpu.touche[0x3]=0;break;}
case SDLK_KP4:{ cpu.touche[0x4]=0;break;}
case SDLK_KP5:{ cpu.touche[0x5]=0;break;}
case SDLK_KP6:{ cpu.touche[0x6]=0;break;}
case SDLK_KP1:{ cpu.touche[0x7]=0;break;}
case SDLK_KP2:{ cpu.touche[0x8]=0;break;}
case SDLK_KP3:{ cpu.touche[0x9]=0;break;}
case SDLK_RIGHT:{ cpu.touche[0xA]=0;break;}
case SDLK_KP_PERIOD:{cpu.touche[0xB]=0;break;}
case SDLK_KP_MULTIPLY:{cpu.touche[0xC]=0;break;}
case SDLK_KP_MINUS:{cpu.touche[0xD]=0;break;}
case SDLK_KP_PLUS:{cpu.touche[0xE]=0;break;}
case SDLK_KP_ENTER:{cpu.touche[0xF]=0;break;}
default:{ break;}
}
break;}

default:{ break;}
}
}

return continuer;
}

void pause()
{
Uint8 continuer=1;

do
{
SDL_WaitEvent(&event);

switch(event.type)
{
case SDL_QUIT:

continuer=0;
break;

case SDL_KEYDOWN:

if(event.key.keysym.sym==SDLK_p)
continuer=0;
break;

default:   break;
}
}while(continuer==1);

SDL_Delay(200); //on fait une petite pause pour ne pas prendre le joueur au dépourvu
}

Le code final diffère légèrement des codes vus plus haut car j'ai moi-même ajouté de nouvelles fonctionnalités à mon émulateur. Vous verrez de nouvelles variables et fonctions qui ne sont pas indispensables. Essayez d'écrire votre propre code afin de vous améliorer.

Si vous désirez tester le code, il suffit de créer un dossier dans le même emplacement que l'exécutable. Nommez-le « SON » et placez-y un fichier son au format WAV. Le fichier son devra être nommé « beep ».

V-B - La compatibilité

Si vous testez certains jeux, notamment blinky et blitz, vous verrez que l'émulateur ne peut pas les faire fonctionner. C'est un problème de compatibilité qui se pose.

Je le dis et je le répète, les informations sur lesquelles nous nous basons pour construire notre émulateur ne sont pas unanimes. Certains jeux utilisent donc vraisemblablement des propriétés que nous n'avons pas implémentées.

Mais en bidouillant, j'ai remarqué que le jeu blitz boguait à cause du modulo dans la fonction de dessin sur l'écran. Donc, pour ce jeu, tout ce qui sort de l'écran ne doit pas être redessiné.

De même pour blinky, il faut augmenter la vitesse de jeu pour obtenir un bon rendu ; d'où l'intérêt de développer un émulateur configurable.

Je ne laisse pas de fichier .zip à télécharger pour la bonne et simple raison qu'un émulateur Chip 8, ce n'est pas ce qui manque, et vous êtes censés programmer le vôtre.

V-C - À suivre

Vous n'avez là qu'une petite esquisse de l'émulation console. Plusieurs notions n'ont pas été abordées puisque n'étant pas utilisées par la Chip 8. En vous souhaitant une bonne continuation, je vous donne quelques pistes à suivre…

V-C-1 - Le son

L'émulation du son n'est pas aussi facile qu'on pourrait l'imaginer. La Chip 8 est un mauvais exemple dans ce domaine. Gardez en tête qu'il faut, dans la plupart des cas, générer le son grâce aux instructions du jeu sans faire appel à une ressource externe.

V-C-2 - Les cycles

Dans notre bloc switch, nous avons défini le même temps d'exécution pour tous les opcodes ; toutefois, certaines consoles introduisent la notion de cycle d'horloge.

En effet, il se peut que certaines instructions prennent plus de temps à être exécutées que d'autres. Il faudra dans ce cas trouver des astuces afin d'assurer une émulation optimale.

V-C-3 - Débogueur

Le nombre d'instructions de la Chip 8 est relativement faible. C'est d'ailleurs ce qui a motivé mon choix de la traiter. Pour les nouvelles consoles (même des anciennes), le nombre d'instructions est énorme, il sera donc utile de créer un débogueur pour voir quels sont les opcodes qui ne fonctionnent pas comme vous le souhaitez. Cela vous facilitera grandement la tâche.

V-C-4 - Optimisation

Certaines consoles sont cadencées à une fréquence tellement élevée qu'une simple approche avec un bloc switch ne pourrait satisfaire. Il existe une méthode appelée recompilation dynamique (dynarec) qui permet de contourner cet obstacle. Cette méthode, bien qu'un peu compliquée, offre des performances inégalables !

V-D - Plus loin

Vous avez fini le tutoriel et vous en voulez encore plus ?

La Chip 8 a un descendant appelé SuperChip 8. Vous pourrez facilement trouver tout ce qu'il vous faut pour l'émuler.

D'ailleurs, la SuperChip 8 partage beaucoup d'instructions identiques avec la Chip 8, vous pourrez donc mettre à jour votre émulateur Chip 8 avec seulement quelques modifications.

La Superchip 8 a aussi un descendant appelé Mégachip 8.

Voici un système tout nouveau, CHIP 16, qui est conçu spécialement pour les débutants en émulation. Ce n'est pas mal pour un second émulateur. Le système est en couleur et est très agréable. Toutes les informations qu'il faut pour l'implémenter sont disponibles. Vous pourrez donc espérer une compatibilité de 100 %.

Nous voici à la fin de notre premier épisode. Vous venez de faire vos premiers pas dans le monde de l'émulation console et j'espère que ce tutoriel vous a été utile.

Merci de m'avoir lu, je vous donne rendez-vous bientôt pour une nouvelle aventure.

Si vous regroupez toutes les citations utilisées, vous retrouverez la présentation entière de Wikipédia. Eh oui, on vient de traduire le document en langage machine. Donc, lorsque je disais :

C'est grâce à ce document que nous allons programmer notre émulateur ; nous allons le traduire en langage machine.

… j'avais entièrement raison et vous pouvez en témoigner.

Vous voilà fin prêts pour vous aventurer dans le monde de l'émulation console.

Ce tutoriel, loin d'être exhaustif, ne représente qu'un aperçu de ce vaste domaine. Après cette initiation, vous êtes en mesure de programmer des émulateurs bien plus complexes du point de vue architectural sans problème.

Comment programmer un émulateur ?

Vous devez me dire sans hésiter qu'il suffit de :

- trouver les caractéristiques de la machine ;

- traduire le tout dans le langage de programmation de votre choix.

Mais j'insiste : pour réaliser un émulateur Sony Next Generation Portable (NGP) ou Nintendo 3DS (ou même PS3 ou XBOX 360 pour les plus téméraires), il faudra creuser un peu plus.

Navigation

Tutoriel précédent : exemples avec quelques instructions

Sommaire