L'émulation, comment ça marche ?
Avant de rentrer dans le vif du sujet, un bref rappel s'impose. Si l'on peut dire çà et là des banalités sur ce sujet très apprécié, on reste toujours sur sa faim. Nous allons essayer de montrer clairement les principales subtilités du fonctionnement interne d'un émulateur. D'abord, il est bon de rappeler qu'un émulateur est un programme comme un autre . Ils ne sont pas plus mystérieux qu'un traitement de texte ou qu'un jeu vidéo. Les émulateurs qui nous intéressent aujourd'hui sont les émulateurs d'ordinateurs. Comment faire revivre les jeux de nos bon vieux Amstrad, Atari, ou Amiga sur nos beaux PC flambant neuf ?
Le temps et sa flèche
Les programmeurs d'émulateurs sont souvent de grands philosophes, car comme eux, ils sont confrontés a la douloureuse question du temps . Ainsi, Aristote aimait a répéter « tout émulateur se doit de fonctionner au minimum a la fréquence de la machine émulée ».. Le paramètre « temps » est omniprésent dans le fonctionnement d'un émulateur. Si l'émulateur tourne moins vite que la machine d'origine, les images apparaissent saccadées. Le son déformé, ralenti comme un vieux 45 tours passé a 33. Si on considère les images comme un signal au même titre que le signal sonore, on se rend mieux compte de l'impact qualificatif résultant d'une tout émulation
Choix du langage
Il convient donc de bien garder a l'esprit le facteur vitesse. Toutefois en pensant aux composants d'un ordinateur ( Processeur, co-processeurs. ) on ne peux s'empêcher d'y voir une analogie avec la programmation d'objet. Sur les processeurs rapide d'aujourd'hui ( Intel Pentium III, par exemple ) on peut se permettre d'écrire le corps du programme en C++. Ces langages offre en effet tous les avantages d'une programmation claire et structurée. De plus, les compilateurs récents produisent un code très rapide, du fait qu'ils maîtrisent parfaitement les nombreuses règles d'optimisation. Pour certaine traitements longs et répétitifs ( conversion d'un format d'écran vers un autre, par exemple ), on pourra utiliser directement l'assembleur. Si elle commence a devenir marginale, l'utilisation du langage machine s'avère parfois indispensable pour certaines tâches très spécifiques. ( Pour la mise à jour des flags qui ne sont pas accessibles aux langages évolués, par exemple ).
La mémoire
Les émulateurs fonctionnant sur des machines plus puissantes que les machines émulées, la quantité de mémoire ne posera pas de problème dans la majorité des cas. Pour émuler une machine de 4 Mo, on allouera juste un buffet de la taille souhaitée. Par la suite, toute opération de lecture ou d'écriture dans la mémoire devra passer par des fonctions ( « WriteMem », « ReadMem »). Elles se chargeront de vérifier la validité des adresses et de les convertir en véritables pointeurs dans le buffer alloué. Ce qui donne par exemple :
U8 *pRamBuffer = (u8*) malloc (*1024*1024) ;
U8 *ReadMem (U32ad)
{
if (ad > = 4*1024*1024) return 0xff ;
return pRambuffer [ad] ;
}
Ici, à titre d'exemple, on s'est contenté de renvoyer la valeur 255 si l'adresse n'est pas valide . La fonction est, en réalité, un peu plus volumineuse car elle doit traiter les erreurs bus et les accès aux co – processeurs, mais nous reviendrons sur ce sujet.
Emulation du processeur
L'exécution d'une instruction sur la majorité des processeurs se déroulent selon le shema suivant :
• Fetch et décodage de l'instruction.
• Exécution de l'opération.
• Mise à jour des flags.
La première étape est très simple, on lit l'opcode de l'instruction en cours et on incrémente le compteur de programme, puis, selon la valeur de l'opcode, on branchera le programme vers les routines appropriées ( addition, soustraction, etc. ). La seconde phase est tout aussi simple, mais rébarbative : il faut émuler à proprement dit chaque instruction. Attention : Les flags émulés ne sont peut être pas mis a jour automatiquement sur le processeur qui émule. Par exemple l'instruction « MOVE » ne touche pas au flag « CARRY » sur un 68000 alors qu'il est mis a zéro sur un x86. Il faudra donc « recalculer » manuellement l'état de certains flags pour quelques instructions.
En résumé, l'émulation du processeur est la partie la plus volumineuse dans un émulateur, mais bizarrement, la moins amusante. C'est une étape longue, fastidieuse dans bien des cas, et très propice aux bugs en tous genres. Pour ceux qui se lancent dans l'émulation, sachez qu'il existe déjà de très bons kits de processeurs dans le domaine public. En récupérer un peut être une bonne idée, si l'émulation de la machine elle-même vous intéresse plus que celle de son processeur.
Les co-processeurs
Les co-processeurs sont a mon goût, les composants les plus agréables à émuler. D'abord parce que l'émulation d'un co-processeur est souvent moins fastidieuse que celle d'un processeur, ensuite, ils sont très divers et leur programmation est donc variée. Un processeur sonore et un processeur vidéo ne se ressemblent pas beaucoup !
Architecture
Tout émulateur est structuré en général autour de la frame. Une frame est le temps de balayage de la machine originale ( souvent 50Hz, soit 20ms ). L'instant d'une frame, l'émulateur doit être capable d'émuler 20ms d'instruction de la machine originale, de construire l'image en cours, et de calculer le son en cours. L'architecture d'un émulateur primaire ressemblerait alors un peu a ceci
While (1) // boucle infinie sur les frames
{
cpu.emulate ((20 * 8000000)/1000) : // nb de cycles pour les 20ms a 8Mhz
video.render () ;
}
Imaginons qu'e l'écran soit noir en début de frame. Six ms plus tard, le processeur exécute une instruction qui va changer la couleur de fond en rouge. Quand on arrive sur la ligne « vidéo.render () », la fonction va tenter de construire l'image écran en prenant en compte l'état interne du co-processeur et de la mémoire vidéo. La couleur de fond, en fin de frame, est rouge. On va donc construire la représentation d'un écran tout rouge, alors que sur la machine originale le changement a eu lieu a 1ms du début de la frame. C'est-à-dire que l'écran devrait être noir sur la première moitié, puis rouge sur la seconde. Comment faire ? On pourrait tout émuler au cycle près, a savoir :
While (1) // boucle infinie sur les frames
{
cpu.emulate (1) ; //Emule cycle par cycle
vidéo.emulate (1) ; // m.a.j d'un seul pixel de l'image
}
Théoriquement, cela fonctionne parfaitement mais en pratique on aurait une catastrophe ! La principale problème réside dans le fait que nos machines actuelles ne sont pas assez puissante ( il y a des changements et des sauvegardes de contextes volumineux a chaque fois que l'on entre ou sort d'une émulation de processeur ). Toutefois, cette architecture sera sûrement viable dans quelques mois ou années pour émuler des machines A1tari ou Amiga, par exemple. Si la solution précédente est une très bonne solution, elle ne reste malheureusement que théorique dans la plupart des cas, du moins aujourd'hui.
Comment faire en pratique ?
Pour etre rentable, l'émulation du processeur central doit tre appelée sur une durée la plus longue possible. Appeler sur le nombre total de cycle d'une frame n'est donc pas une mauvaise idée, mais pour pallier au problème du timing de l'accés aux co-processeurs, on va devoir modifier un peu l'émulation du processeur central. L'idée est de noter dans uen table le cycle et la valeur de chaque accés a un co-processeur. Dans notre exemple précédent, l'&état de la couleur de fond était noir en début de frame. 10ms, rouge. Si a 15ms du début de la frame le processeur change encore en bleu, on le note. on aura ainsi en fin de frame, une table qui ressemble a :
0 ms, noir
10 ms, rouge
15 ms, bleu
Notre routine « video.render () » va relire cette table, et pôurra donc bien tracer une bance noire, puis une rouge et enfin une bleue. En pratique, on ne notera pas le temps en ms mais en cycle à partir du début de la framme, et la couleur sera bien sur une simple valeur numérique. On peux utiliser ce format de table pour chaque co-processeur, a savoir un numéro de cycle, une adresse ( couleur de fond, résolution, est…) et une valeur. Voici un exemple de cette structure
Typedef struct
{
U30 nbCycleBefore ;
U32 address ;
U30 value ;
} writeacces_t ;
Comment remplir cette tabler d'accès?
Rappeler vous de la fonction « WriteMem ». Dés que le processeur central fait un accès en écriture, cette fonction est appelée. Suivant l'adresse passée en paramètre, plusieurs cas de figures se présentent.
• L'adresse est une adresse valide en RAM, auquel cas on écrit physiquement la valeur dans le « pRamBuffer ».
•L'adresse est un registre d'un co-processeur. On va alors ajouter une entrée à la table d'accès de ce co-processeur. On y écrit le cycle en cours ( l'émulateur de processeur garde toujours a jour un compteur de cycle ), l'adresse au sein du co-processeur et la valeur.
• L'adresse est invalide, on effectue un traitement spécifique selon la machine émulée. ( rien ne se passe, une erreur bus se déclanche, etc )
Comment relire la table ?
A la fin de l'émulation processeur d'une frame, les tables de chaque co-processeur sont remplies. Il faut donc les relire et les interpréter. Chaque co-processeur possède des variables qui définissent son état interne a un instant donné. Prenons le cas concret de notre co-processeur vidéo simplifié et disons qu'il ne possède qu'un seul registre : la couleur de fond d'écran. Pour construire l'image émulée, on va tracer cette couleur de fond jusqu'au cycle de la prochaine entrée dans la table. Arrivée a ce cycle précis, on va lire la nouvelle couleur de fond dans la table, puis continuer. Ainsi, l'état interne du co-processeur vidéo va changer au cours de la construction de la framme. Voici le pseudo code de la fonction d'émulation de notre co-processeur vidéo :
Tant que la frame n'est pas terminée
COLOR = valeur dans la table
N = Nb de cycles dans la table avant prochain accés
Trace de pixels de couleurs COLOR pendant N cycles.
Avance d'une entrée dans la frame
Fin
Cette fonction est efficace et traite parfaitement le cas des changements de couleur pendant la frame. Dans la pratique, les choses sont un peu plus compliquées car un processeur vidéo n'a généralement qu'un registre de couleur. Vous y trouverez souvent tout un tas de registres de résolution. La fonction sera donc plus compliquée car il faudra prendre en compte tous ces différents changements durant la construction graphique de l'image. De plus, la mémoire écran de la machine émulée n'a souvent pas du tout le même format que l'écran utilisé pour l'émulation ( ex : Amstrad, Atari, Amiga émulé sur PC ). L'important est que nous ayons cerné l'essentiel. Tout ce que nous avons vu pour la vidéo est aussi valable pour le son, ou n'importe quel autre co-processeur. Cette architecture résout donc efficacement le problème de l'écriture dans les co–processeur au cycle près. Avons-nous terminé ? Qu'en est il de la lecture des co-processeurs ?
Lecture dans les co-processeurs
Si l'émulation de l'écriture dans les co-processeurs au cycle près est indispensable pour émuler correctement des jeux ou des démos ( exemple avec couleur de fond ), celle de la lecture est moins évidente. Aujourd'hui, peu d'émulateur gère ce cas correctement. Imaginons que notre co-processeur vidéo émulé possède un compteur de ligne, c'est-à-dire un registre que le processeur peut lire et qui indiquerait en permanence le numéro de la ligne actuellement balayée par le faisceau d'électron. Qui va mettre a jour ce compteur de ligne dans l'émulateur ? L'émulation du processeur est lancée sur une frame complète. Durant tout ce temps, la valeur du compteur de ligne reste identique dans l'émulateur, alors qu'elle change à quelque ligne dans la réalité !! Si un programme utilise cette valeur pour une raison quelconque, il risque de ne pas fonctionner sur votre émulateur. Dans le cas simple de notre compteur vidéo, on peut résoudre le problème en modifiant l'émulation du processeur. Si dans notre exemple une ligne fait 320 cycles, il suffit donc d'incrémenter une variable tous les 320 cycles depuis le corps de l'émulation du processeur central. Ainsi cette variable est elle toujours a jour dans la frame. Vous devez commencer à vous y habituer, mais une nouvelle fois, les choses sont plus délicates que dans la réalité. En effet, reprenons l'exemple de notre compteur vidéo, en imaginant maintenant que le co-processeur de la machine possède 3 résolutions différentes. Selon la résolution en cours, le co-processeur ne mettra plus 1 cycle par pixel. Le maintien de la valeur du compteur vidéo dans l'émulation du processeur central va donc dépendre fortement du fonctionnement même du co-processeur vidéo ! Tout programmeur sait que l'imbrication de modules aussi différents ne peut apporter que des ennuis à la longue. Alors comment faire ?
Vive les tables !
Rappelez vous que l'émulation du processeur central maintient déjà a jour une table d'accès en écriture pour chaque co-processeur. Pourquoi alors ne pas réutiliser cette table pour « recalculer » la valeur de chaque registre du cycle demandé ? Revenons un peu a notre fonction « ReadMem ». Si on lit le registre d'un co-processeur, on ne va pas renvoyer directement sa valeur actuelle car elle a toutes les chances d'être fausse ( souvenez vous de l'exemple du compteur vidéo ). Au lieu de cela, on va relire et réinterpréter la table d'accès en écriture ( Qui est en train de se construire je vous rappelle que nous sommes encore dans « cpu.emulate (n) » pour trouver la valeur exacte du registre. Ce registre peut être un compteur vidéo, une adresse du ample en cours dans le cas d'un DAC, un compteur d'un timer ou toute autre bizarrerie. Bien sûr, vous allez me dire que réinterpréter la table a chaque lecture dans un co-processeur est un processus lent. Oui, sauf que vous n'êtes pas obligé de relire la table depuis le début a chaque fois ! Comme la table se construit au fur et a mesure, vous n'avez qu'a la ré interpréter que depuis son dernier accès. En pratique, c'est donc très rapide !
Au final
Nous voici a la fin de notre périple initiatique. Nous espérons avoir levé un morceau du voile mystérieux qui recouvre le fonctionnement de ces curieuses bêtes. Peut être avons-nous aussi suscité des vocations chez ceux qui deviendront les auteur des émulateurs de demain !
...........PC TEAM HORS SERIE N°10 JUILLET 2001.......Fdasi-amigaland








