Homemade Pixels

Des pixels frais qui sortent du four le blog de MrHelmut.

So, you want to code an emulator ?

Cet été, j’ai découvert qu’il existait un émulateur Wii très performant, Dolphin. Cela faisait un moment que je n’avais pas guetter la scène émulation. Aux dernières nouvelles que j’avais prises, il existait un émulateur Gamecube, des plus bancal (c’était déjà le projet Dolphin). Autant vous dire que j’ai pris une claque en lançant cet émulateur Wii sur ma machine (un peu comme à l’époque où Chankast avait débarqué de nul part). Cerise sur le gâteau, il est open source. Alors quand j’ai mis la tête dans le code, je me suis dit : MER IL SON FOU.

J’adore comprendre les choses, ce qui se passe en “behind the scene”, ce que mon processeur fait si je lui dis de faire A + B. J’ai codé 1001 choses dans ce but, mais je ne sais pas pourquoi, les émulateurs ont toujours eu quelque chose de mystique pour moi. De mon approche naïve, faire un émulateur consiste à écrire un programme qui simule en 1:1 les composants d’une machine. Mais ce que j’ai vu dans le source de Dolphin n’avait rien à voir. Il ne reproduit pas les composants, mais recompile des opcodes PowerPC vers x86 (facon JIT). Bref, des dingues, mais diable que c’est performant. La plupart des bons émulateurs font des choses de ce genre.

Bien entendu l’approche naïve est viable pour de petits hardware. Je me suis donc armé de mes connaissances en archi hardware et assembleur pour créer mon propre émulateur. Ainsi, je me suis attaqué à un système simple : la Gameboy. Il y a plein de systèmes sympa pour débuter, les Atari, la Nes, etc, mais la GB a l’avantage d’avoir un hadware très largement documenté via ce qu’on appel la “pandocs”, dont un miroir est trouvable par ici, mais aussi de la doc sur les instructions CPU.

Le résultat, c’est SilverGameboy, un émulateur codé en C# (avec une sortie Silverlight ou XNA suivant la version), tournant sur PC/Xbox360/WindowsPhone. Je compte ouvrir les sources dès qu’il aura atteint un degré de finition satisfaisant.

SilverGameboy

Ce qu’il lui manque encore pour être “fini”, en dehors d’habituels bugfix :

  • Le support du rom banking (pour l’instant uniquement MBC1, avec des bugs, donc 90% des jeux encore injouables)
  • Le son (assez problématique sur Xbox360 et WP7, car pas d’accès bas niveau à la couche sonore)
  • Tout ce qui touche à la Gameboy Color (et le Super Gameboy, mais ca je crois que je ne vais pas le couvrir)
  • Et j’aimerais bien faire un wrapper réseau du port série pour jouer en multi (devrait être assez trivial)

Les petits trucs que j’ai trouvé “marrants” en le faisant :

  • Le CPU possède ~512 instructions, que j’ai mis quelques heures à écrire et… plusieurs jours à déboguer. Heureusement pour ca il existe des ROMS de test qui testent rigoureusement les instructions une par une, le bon comportement des registres etc. Malheureusement les tests ne sont pas très explicites en dehors de dire “passed” ou “failed” alors que 200 instructions peuvent être erronées :-/. Je m’en suis sorti en “trichant”, j’ai pris les sources d’un autre émulateur que j’ai modifié pour avoir une trace d’exécution, que j’ai comparé avec une trace de mon propre émulateur pour comprendre ou ca clochait. Je me suis retrouvé à faire des “diff” sur des fichiers de traces de 500Mo. La où le bas blesse, c’est que les documentations sur le CPU de la GB sont contradictoires : d’une doc à l’autre, les “flags” et les timings ne sont les même. Je ne comprenais pas pourquoi d’un émulateur à l’autre, ils implémentaient des instructions si différemment. Comment faire pour être sur de coller au hardware ? Coder un test GB et demander à un copain qui a un SDK Gameboy de l’exécuter sur un vrai hardware…
  • Le rom banking, c’est barbare. Le rom banking, c’est une méthode des plus utilisées pour palier à des limitations hardware. La Gameboy ne peut adresser que 64ko à la fois (dont 32 dédiés à la ROM d’un jeu). Or, certains jeux font jusqu’à 2mo. Les cartouches sont alors découpées en plein de “bank” mémoire de 32ko, et au besoin on les échange pour aller taper dans la totalité. Ce que j’ai trouvé spé, c’est la facon dont on change de bank. Dans chaque cartouche de Gameboy, il y a une puce, qui gère les banks. Les cartouches sont de la mémoire en lecture seule (normal, sinon on pourrait modifier des jeux), or, il est possible d’”écrire” dessus. C’est la puce qui gère les banks qui va comprendre que en écrivant, vous voulez changer de bank (mais ne va pas vraiment écrire). Je ne sais pas comment le rom banking est géré sur d’autres hardware, mais le coup de la nécessité d’une puce dans chaque cartouche et le principe de faire une fausse écriture pour changer de bank, je trouve ca barbare. Mais il y a plus pourri, il existe plusieurs versions de cette puce (appelé Memory Bank Controller, aka MBC), et forcément, chaque version a un comportement différent. Il en existe une petite dizaine et certaines sont très mal documentées.
  • Emuler un vieux système revient à émuler une télé ^^. Dans le cas de la Gameboy, le hardware est étroitement lié au fonctionnement de l’écran LCD. Cet écran a des caractéristiques précises, il fonctionne comme un tube cathodique, en balayant les lignes de pixels. Chaque balayement a une durée précise, et suivant l’état de l’écran, le CPU a le droit de taper dans la mémoire graphique. Il faut donc coder un pseudo-écran LCD. J’ai regardé comment ca se passe ailleurs, notamment sur Atari, même combat, c’est assez lié au comportement d’une cathodique. Je pensais me retrouver face à des systèmes assez banals, avec un framebuffer et une indépendance de la sortie vidéo, mais non. C’était marrant.

Mais tout ca ne répond pas à la question initiale de ce billet. Quand on parle de développement d’émulateurs, c’est un peu comme le développement de jeux vidéo de façon générale. Il y a toujours pleins de kevins qui croient que avec 2 semaines de cours en BAC+1 on peut développer un MMORPG. Il faut commencer petit.

Bref, tout ca pour dire que je m’adresse uniquement aux personnes conscientes qu’on ne code pas un émulateur en ne voulant pas s’intéresser au bas niveau. Pour se lancer dans l’émulation, il est préférable d’avoir des bases d’architecture hardware, savoir un peu de comment on est arrivé de la logique booléenne à des processeurs. Loin d’être indispensable, mais préférable pour savoir où l’on va. Pour ces gens curieux, et qui veulent commencer doucement, il existe une “machine” à émuler. Il s’agit du Chip8. En fait, cette machine n’existe pas. C’est une machine virtuelle qui a été inventée dans le but de simplifier le développement de jeux dans les 70’s. Il y a une doc disponible sur son hardware virtuel, et la machine possède un jeu d’instructions très limité (une trentaine, contre les 512 de la GB par exemple), ce qui en fait un hardware de choix pour commencer à comprendre l’émulation. Il y a donc tout ce qu’il faut par ici pour bien débuter. Si la demande suit, je vous pondrai un tutoriel sur le développement d’un émulateur Chip8.

Mise à jour :

Suite à vos remarques sur la clareté de mon paragraphe sur le rom banking, voici quelques précisions sur la mémoire de la Gameboy.

Les images étant plus larges que 800px, il est préférable de lire cet article sur une seule colonne.

Une mémoire, ce sont des blocs de 1 octet (8 bits) qui sont tous numérotés. La Gameboy a un bus mémoire capable de voir 65536 blocs (autrement dit, 64ko).

blocks
Or, la Gameboy possède seulement 8ko de mémoire vive. A quoi servent le reste des 56ko ? Comme l’a souligné LeGreg dans les commentaires, le bus mémoire est de type “memory-mapped I/O”. Une partie est donc dédiée aux I/O (gamepad, port série, son, interruptions…), mais aussi à la RAM et à lire les cartouches de jeux (32ko de cet espace, divisé en deux parties de 16ko, est dédié à la lecture des cartouches). Le reste est grosso inutilisé (ou presque).

memorymap

Comme je l’ai expliqué, 99% des jeux Gameboy font plus de 32ko. Comment faire avec cette limitation hardware ? Il y a un mécanisme, qui est interne aux cartouches, et non à la GB : le Rom Banking. Le rom banking est géré par une puce à l’intérieur de chaques cartouches, le Memory Bank Controller (MBC). Son rôle est de montrer à la Gameboy uniquement 32ko de la ROM. La ROM est alors découpée en “bank” de 16ko (j’avais simplifié ce cas par des banks de 32ko dans mon article, ce qui est faux en fait). La bank 0 est toujours visible par la GB, afin d’avoir accès au programme principal en toutes circonstances. Par contre, la “bank n” est amovible, et c’est le MBC qui la pilote.

mbcMais comme la Gameboy n’est pas capable de gérer ça d’elle même, il faut pouvoir dire au MBC de montrer la bank que l’on souhaite. Le principe est alors d’écrire sur la cartouche (ROM), une opération illégale. Le MBC va alors intercepter cette écriture et l’empecher. A la place d’écrire, le MBC va comprendre par votre tentative illégale, que vous souhaitez voir une autre bank.

mbc21
Un bus mémoire de type memory-mapped I/O est quelque chose de tout à fait commun. Le rom banking également. Ce que j’avais trouvé bizarre, c’est que ceci n’est pas implémenté dans le hardware de la console elle même, mais dans les cartouches de jeux, via une puce dédiée (le MBC), et via le principe bizarre de “fausse écriture” (et du fait qu’il existe plein de versions de cette puce).
Je ne sais pas de quelle manière le rom banking est géré ailleurs, mais je m’attendais à quelque chose de plus propre en terme de design (comme un bête registre CPU). Après il se peut que ce soit comme ca partout et qu’il y ai une bonne raison à cela, mais je n’ai pas creusé la question.

16 commentaires pour “So, you want to code an emulator ?”

  1. Tarto dit :

    Donc le rom banking écrit-il ou pas sur des zones de la cartouches ?
    Tu parle de fausse écriture, mais ou les données sont-elle écrites ?

    Je trouve que l’explication n’est pas vraiment claire…

    Sinon ça va le C# pour de l’émulation hardware ? Pour en avoir fait un peu de XNA je suis un peu dépaysé en venant du C/C++.

  2. MrHelmut dit :

    En fait, ca n’écrit pas. Quand tu veux écrire sur la ROM, le MBC comprend que tu veux changer de bank, mais ne procède pas à l’écriture.
    Donc dans ton code, tu te retrouve avec une écriture, mais celle-ci n’est pas effectuée. C’est assez déroutant dans le principe.

    Le C# pour l’émulation, ca va très bien. Mine de rien c’est relativement performant. Ce n’est pas du C, c’est sur, mais c’est pas mal avec plein d’opérations bitwise et les types qui vont bien.
    Son problème, dans le cas de mon émulateur en tout cas, c’est qu’il ne supporte pas l’inlining manuel de méthodes. Pour l’instant, mes instructions sont dans des méthodes séparées qui ne sont pas inlinées. J’ai préféré un code lisible et maintenable à une bouillie performante.
    Le problème vient plus des API graphique, notamment Silverlight/WPF, qui est loin d’être la panacée pour faire des boucles de rendus (d’un côté, ce n’est pas fait pour).

  3. neFAST dit :

    MrHelmut a dit :
    So, you wan to code an emulator ?

    So, you wan to spea englis?
    Article interessant qui n’a qu’un seul défaut : on aimerait avoir plus de détails (sans aller jusqu’à un tutoriel comme tu l’évoques).

  4. MrHelmut dit :

    Yep, merci pour la typo, c’était honteux.

    A vrai dire, je ne sais pas encore à quel type de public je m’adresse via wefrag, donc on verra pour des choses plus détaillées :).

  5. LeGreg dit :

    Tu es sûr que ce n’est pas un simple mmio :
    http://en.wikipedia.org/wiki/Memory-mapped_I/O

    Pour s’adresser au périphérique tu as soit une instruction spéciale (inp, outp), soit une adresse “standard” remappée vers une entrée sortie du processeur (memory mapped io).

  6. MrHelmut dit :

    La GB a effectivement un bus mémoire de ce type, une petite plage d’adresses étant dédiée aux registres d’I/O.
    Le rom banking, c’est encore autre chose.

    Je vais mettre à jour l’article dans la journée, avec plus d’explications sur la mémoire.

  7. Yupa dit :

    Super article! Je suis intéressé par le chip8 et par plus de détails sur ton ému GB dans tes prochains articles.

  8. MrHelmut dit :

    Merci pour tous vos commentaires.

    Je viens de mettre à jour l’article avec des détails sur le rom banking et le bus mémoire. J’espère que cela clarifie mon explication bancale.

  9. skaven dit :

    Superbe article. J’adore et j’en veux encore. Encore ! ENCORE!

  10. LeGreg dit :

    “Je ne sais pas de quelle manière le rom banking est géré ailleurs, mais je m’attendais à quelque chose de plus propre en terme de design (comme un bête registre CPU). Après il se peut que ce soit comme ca partout et qu’il y ai une bonne raison à cela, mais je n’ai pas creusé la question.”

    Les designs bizarre sont une caractéristique constante des vieux systèmes ce qui leur permet souvent de faire des trucs qui ne seraient pas accessible sinon (ma pensée pour l’amiga..).

    Plusieurs choses peuvent venir à l’esprit : limitation des registres CPU, sauver de l’espace d’adressage, ou peut-être que la console a été conçue pour des ROMS de 32ko de base, puis les demandes des jeux augmentant avec le temps un mécanisme d’extension rétro-compatible a du être mis en place.

  11. Nioub dit :

    Certaines cartouches ont une partie inscriptible (une EEPROM), par exemple pour gérer les sauvegardes des parties. Hors le processeur/chipset n’a aucune connaissance de comment est faite la ROM, s’il y a des parties inscriptibles, leurs position et taille, etc. C’est pour cette raison que la puce gérant le MMIO va gérer tout ça, à la demande du code de jeu.

    Ensuite, on utilise une bête écriture mémoire comme switch pour éviter de devoir rajouter une instruction dédiée dans le processeur, ce qui veut dire re-designer le processeur, le tester complètement, ce qui prend beaucoup de temps et d’argent.

    Enfin, utiliser l’écriture mémoire a un défaut (variable selon le processeur) : la valeur écrite va polluer le cache données car le processeur présumera que l’écriture a réussi, et tant que la donnée restera en cache la ROM ne sera pas lue à cet endroit. Je peux me tromper, ces vieux processeurs peuvent avoir une exception pour ne pas mettre en cache la ROM, et si l’on lit la ROM juste adjacente à la valeur cela forcera une relecture.

  12. GonMad dit :

    Utilises-tu des blocs de code unsafe en c#, ou est-ce qu’ils peuvent apporter de meilleures performances ?

    Superbe article, impatient de jouer avec la solution.

  13. GrinderFurax dit :

    Super article.
    J’en redemande :)

  14. MrHelmut dit :

    @LeGreg : ca semble logique, je pense aussi que la demande de jeux de plus de 32ko est venue après le “taped out” de la GB. Il va falloir que je regarde un peu d’autres vieux hard :).

    @Nioub : je n’avais pas pensé au cache effectivement. Je peux me tromper, mais dans le cas de la GB, il n’y a pas de cache, ou du moins je ne crois pas que ces écritures fantômes polluent le cache. Pour la RAM inscriptible, ceci est décrit dans le header de la ROM, si la cartouche en possède (il y a aussi des cartouches qui ont 8ko d’extension de ram). Le CPU est effectivement aveugle, mais c’est au dev de faire le boulot de savoir quelle type d’EEPROM il y a.

    @GonMad : pas de code unsafe, car ceci est interdit sur WP7 / Xbox360. Enfin, ca reste possible en “jailbreaké”, mais si un jour je le publie sur le market, il sera refusé à cause de ca. Ca pourrait aider les perf essentiellement pour ce qui est de la sortie graphique, pour aller taper directement dans une texture ou un framebuffer de Silverlight. Mais comme l’ému est figé à 60FPS et que le rendu graphique se fait dans un thread séparé, je ne sais pas si il y a un intéret de ce côté. Il y a surement beaucoup plus de choses à optimiser côté CPU, notamment ma gestion du “memory mapped I/O”.

  15. gruy-earth dit :

    Ce que j’avais trouvé bizarre, c’est que ceci n’est pas implémenté dans le hardware de la console elle même, mais dans les cartouches de jeux, via une puce dédiée (le MBC), et via le principe bizarre de “fausse écriture”
    Je ne connaissais pas le rom banking mais je trouve l’astuce très intelligente, voici ma tentative d’explication.
    La cartouche n’a pas d’instructions, parce-qu’elle n’a pas de CPU, elle ne peut donc ni lire ni écrire dans la GB, pas même un registre particulier.
    Et donc le SEUL moyen pour “envoyer” une information à la cartouche, c’est d’écrire dedans.

    Ça doit bien chiant d’écrire un programme sachant que des morceaux du codes disparaissent et réapparaissent. Ça oblige - en plus d’avoir à gérer la mémoire - de connaître les numéros de bank de chaque sous routines.
    J’espère que les développeurs de l’époque ont préféré stocker des data plutôt que du code dans ces bank volatiles. :)

  16. MrHelmut dit :

    Oui en effet, il semble que les bank 0 et 1 soient communement attribuées au programme principal. C’est soit un concensus, soit le fait qu’il y ait eu un SDK qui cachait les bank aux développeurs (mais connaissant Nintendo, je pense qu’un SDK évolué n’est arrivé qu’après le lancement de la GB).

    Et comme d’autres l’ont précisé, le rom banking était fréquent dans les anciennes machines pour palier au manque de RAM. Donc en soit, ce n’est pas surprenant (et c’est intelligent comme tu le dis).
    Il faudrait que je regarde si sur NES c’est pareil, ca expliquerait bien des choses sur toutes les consoles 8/16bit de Nintendo.

Laisser un commentaire

Vous devez être connecté avec votre compte Wefrag pour publier un commentaire.