Homemade Pixels

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

Archive pour la catégorie ‘Emulation’

« Articles plus anciens

Il y a peu, un ami m’a demandé quelques détails sur la Gameboy et sa capacité de communication via son port Game Link (qui sert à brancher deux Gameboy entre elles pour jouer à deux). Derrière sa question se cache un projet farfelu mais hautement cool (je reviendrai plus tard sur la finalité de celui-ci) et le topo est simple : comment communiquer avec une Gameboy et autre chose qu’une seconde Gameboy ? (notamment un port 3 broches TTL, ou Transistor-to-Transistor Logic)

Ni une ni deux, je resors ma documentation sur la Gameboy. Au passage, un ami qui dispose d’un SDK officiel m’a passé la doc de chez Nintendo. Véritable perle car elle contient toutes les infos bas niveau (jusqu’aux signaux électriques) que je n’ai pas trouvé sur le net ou dans la quasi-exhaustive pandocs.

Bref, en apparence, le port Game Link de la console ressemble à un port série. Je me dis “chouette, un port TTL, les doigts dans le nez”. Sauf qu’en réalité, ce n’est pas tout à fait du TTL et il y a essentiellement deux contraintes :

  • La Gameboy n’a pas de bits de début et d’arrêt sur le signal de transfert, mais se base sur des impulsions d’horloge (il y a donc 3 fils au lieu de 2 en TTL, si on exclu la masse);
  • Pour envoyer / recevoir des données, la Gameboy n’a qu’un registre 8bit, ce qui implique de devoir traiter ces 8 bits avant de recevoir quoi que ce soit d’autre (donc si un appareil quelconque balance plus de 8 bits d’un coup, la Gameboy ratera tout ce qui arrive le temps de repasser en mode réception). Or, les trames TTL sont plus longues que 8 bits à cause du point précédent et le périphérique visé n’a pas de mécanisme de segmentation de trames (comme peut avoir un clavier PS/2).

La Gameboy ne peut donc tout simplement pas communiquer avec un port TTL (enfin, ça dépend du périphérique et du mode de réception de la Gameboy, mais dans mon cas précis, je suis coincé). D’où l’idée de coller une carte Arduino (qui a un port TTL) entre la Gameboy et l’autre appareil (qui lui est bien TTL). Ca permettra aussi de faire plein d’autres trucs marrants par la suite.

TTL versus Game Link

Un peu d’électronique histoire d’illustrer la différence. Le protocole TTL se base sur deux fils. Un pour l’envoi de donnée (TX) et un pour la réception (RX) et permet une communication asynchrone full-duplex. Les trames sont généralement de 10 bits (1 bit de début, 8 de donnée et 1 bit de fin), mais parfois sans bit de début. Si il n’y a pas de transfert, le signal est HIGH (+5V), les 1 sont codés en +5V et les 0 en 0V. Pour que la communication se fasse, il faut que les deux appareils connectés aient leur port TTL configurés à la même fréquence (sinon on ne peut pas déterminer combien de bits sont passés, à moins d’avoir un signal d’horloge mais ce n’est pas mon cas).

Imaginons que l’on veuille transmettre la série de bits 01001011. Voici le signal correspondant :

signal0

Notons au passage que les données sont transmises en partant du bit de poids faible.

Le Game Link de la Gameboy est différent. Tout d’abord les trames, qui font 8 bits et ne contiennent que des données. Pas de bits start/stop ou de parité (contrôle d’erreurs). Pour savoir si des données sont envoyées ou non, et à quelle fréquence, c’est un signal d’horloge qui donne le la. Ce signal d’horloge n’est donné que par un des deux interlocuteurs (il n’y a qu’un seul câble d’horloge, donc seule une machine peut l’émettre). Celui qui émet l’horloge est dit “master”, l’autre est son “slave”. Bien entendu ces rôles peuvent changer à chaque transaction, mais par simplicité on définit et on garde les rôles une fois pour toute (dans les jeux Gameboy, c’est le joueur qui appui en premier sur “start” qui est “master”). Le comportement électrique est le même que le TTL : 0-5V et idle à 5V.

Le signal d’horloge définit la vitesse à laquelle les données sont transmises et le signal de données doit être parfaitement synchronisé à l’horloge. La Gameboy originale (non color) a une fréquence d’horloge fixe, de 8192hz, soit une capacité de transmission de 1 Ko/sec (overhead de traitement des trames exclu). Mais en mode slave, la Gameboy accepte un signal pouvant aller jusqu’à 500Khz (~60 Ko/sec).

Dans la pratique, voici le même exemple qu’avant avec un envoi des données 01001011 fait par une Gameboy master :

signal

Le slave, qui est en mode écoute, attend le signal d’horloge de la part du master et enregistre tous les bits impulsés par l’horloge (chaque bit entrant est poussé dans le registre 8bit par la droite à chaque impulsion d’horloge).

Si le slave veut envoyer des données, il doit attendre que le master passe en mode écoute et qu’il lui envoi le signal d’horloge.

A l’inverse du TTL, c’est le bit de poids fort qui est envoyé / reçu en premier. Mais ce n’est pas grave en soi, puisqu’il suffit d’inverser la trame à réception ou avant l’envoi.

L’avantage d’être porté par un signal d’horloge, est que la fréquence de transmission peut être variable, même en pleine trame, pourvu que les signaux soient synchronisés. Et ça, c’est vachement pratique pour la bidouille visée avec l’Arduino.

En bref, la compatibilité entre TTL et Game Link se joue à un cheveu près (les bits start/stop). On pourrait ségmenter les trames ou se débrouiller pour ignorer les 2 derniers bits, mais suivant les cas et les périphériques, on ne peut pas.

Arduino to the rescue

Ne pouvant communiquer via le protocole TTL, l’idée est alors de mettre une carte Arduino entre la Gameboy et le périphérique afin de retraiter une entrée TTL pour la rendre compatible Game Link. Grosso modo on retire les bits de contrôle (fait automatique par l’Arduino) et on inverse la trame.

Les Arduino ont des ports séries standards (généralement les PIN 0 et 1), donc côté entrée il n’y a aucun problème. Pour la communication avec la Gameboy, il nous faut donc 3 fils : l’horloge, une entrée et une sortie.

Si l’on veut que l’Arduino puisse fonctionner en mode slave, il faut être capable de détecter le début précis du signal d’horloge et de lire avec un timing tout aussi précis les données sur le fil d’entrée. Or, les Arduino ne permettent pas de détecter le début exacte d’un signal. Juste si celui-ci est HIGH ou LOW, mais pas de savoir depuis quand il est HIGH avec la précision requise. De plus, lire l’horloge, puis le fil d’entrée, se fait en deux étapes (DigitalRead) et il n’est pas garantie que la lecture des deux ait été synchrone. En gros, un Arduino ne peut pas fonctionner en mode slave avec une Gameboy, par limitation technique.

Et le mode master alors ? Pour être master, il faut être capable de deux choses : balancer un signal d’horloge et balancer de manière synchrone un signal d’envoi (ou lire le signal de réception).

Pour ce qui est de l’horloge, les Arduino permettent d’émettre un signal carré facilement, mais pas de déterminer le nombre d’impulsions pour les 8 bits d’une trame. Mais le Game Link a un avantage : pas besoin d’avoir un signal régulier, seule la synchronisation des signaux est importante.

Pour un envoi, l’idée est donc d’émettre un pic de signal d’horloge en même temps que la valeur du bit qui doit être véhiculé (le pic d’horloge ordonne à la Gameboy d’envoyer/recevoir un bit). Mais le problème est le même qu’avec la lecture en mode slave. Faire deux DigitalWrite (un pour l’horloge et un pour l’envoi/réception) l’un après l’autre ne garanti pas la synchronisation des signaux.

Solution ? La programmation bas niveau de l’Arduino en allant directement taper dans les registres des signaux. Inconvénient ? Ce n’est pas toujours portable entre différents modèles d’Arduino et pas spécialement “propre”. Avantage ? Ca permet de gérer la synchronisation de nos signaux aux petits oignons.

Dans la pratique, toute la magie réside dans le code Arduino suivant (désolé pour la lisibilité, mais j’adore les opérations bitwise) :

byte GB_CLK = B00000100; // on définit la PIN 2 comme étant notre horloge
byte GB_IN = B00001000; // PIN 3 sera l'entrée
byte GB_OUT = B00010000; // PIN 4 la sortie
// ATTENTION : les fonctions suivantes ne gèrent que les I/O sur les PIN analogiques 0 à 5 (car
// on bosse uniquement sur le registre Arduino C)
// Par conséquence, tous les signaux GB doivent être associés à des PIN d'un même registre,
// sinon on ne peut pas assurer la synchronisation des signaux

void setup()
{
	// on initialise les PIN en input ou output
	DDRC |= GB_OUT | GB_CLK; // outputs
	DDRC &= ~GB_IN; // inputs
	PORTC |= GB_CLK; // on initialise l'horloge à HIGH conformément à la spec GB
}

void sendToGB(byte data)
{
	for (int currentBit = 0; currentBit < 8; currentBit++) // on émet un bit après l'autre
	{
		if (data & B10000000) // si le bit de poids fort est à 1, on envoi HIGH
		{
			// la génération de l'impulsion se fait en deux temps, une fois pour
			// générer la descente du signal, une fois pour la remontée
			PORTC &= ~GB_CLK; // CLK = LOW
			PORTC |= GB_CLK | GB_OUT; // CLK = HIGH, OUT = HIGH
		}
		else // sinon LOW
		{
			PORTC &= ~GB_CLK; // CLK = LOW
			PORTC |= GB_CLK; // CLK = HIGH, OUT = LOW
		}
		data <<= 1; // et on éjecte le bit de poids fort qu'on vient d'envoyer
	}
}

byte readFromGB()
{
	byte data;
	for (int currentBit = 0; currentBit < 8; currentBit++)
	{
		data <<= 1;
		PORTC &= ~GB_CLK; // CLK = LOW
		// lecture de la PIN
		if (PINC & GB_IN)
		{
			data |= B00000001;
		}
		PORTC |= GB_CLK; // CLK = HIGH
	}
	return data;
}

Et le tour est joué !

Il faudra juste s’assurer qu’entre chaque opération la Gameboy ait le temps de repasser en mode écoute / émission avec des delay côté Arduino.

Après, pour les transactions, il convient de créer son propre protocole.

Bonux 1 : schéma du câble Game Link

Pour brancher la Gameboy à l’Arduino, j’ai pris un câble Game Link que j’ai charcuté pour brancher directement aux PIN nécessaires. Voici le schéma du câble (les couleurs correspondent aux couleurs du câble officiel Nintendo) :

  • CLK, le signal d’horloge, à brancher sur la PIN GB_CLK
  • IN, l’entrée de la Gameboy, à brancher sur la PIN GB_OUT
  • +5V, pas utilisé dans le cas de la Gameboy, ignoré
  • GND, la terre, à brancher sur la terre de l’Arduino
  • SD, ignoré
  • OUT, la sortie de la Gameboy, à brancher sur la PIN GB_IN

Bonux 2 : programmes de test

Pour tester la communication, il faut deux programmes, un côté Gameboy et un côté Arduino. On va se contenter de faire un echo : l’Arduino envoie un octet à la Gameboy qui lui renvoie le même incrémenté de 1.

Programme Gameboy (rapide test en assembleur) :

; Pour l'appel du traitement des données à réception/envoi
SECTION "Interrupetion reception", HOME[$58]
INT_58:
	CALL SERIAL_VECT
	RET

; Fonction main du programme
SECTION "Main", HOME[$0150]
Start::
	LD ($FFFF), $8 ; on active l'interruption de réception (INT $58)
	EI ; on active les interruptions
	LD ($FF02), $81 ; on active l'envoi/réception en mode slave
LOOP:
	JP LOOP ; on boucle en attendant l'interruption de réception

; La fonction de traitement à réception
SERIAL_VECT:
	LD A, ($FF01) ; on récupère l'octet reçu
	INC A ; on incrémente la valeur de 1
	LD ($FF01), A ; on replace la valeur dans le registre de données du Game Link
	LD ($FF02), $81 ; on repasse en mode envoi/réception slave
	RET ; on retourne à la boucle pour attendre l'envoi (puis on ne fait plus rien)

Côté Arduino :

// à rajouter dans setup()
void setup()
{
	...
	Serial.begin(9600);
}

// globals
byte result = 41;

void loop()
{
	sendToGB(result);
	delay(10);
	result = readFromGB();
	Serial.println(result); // si 42 s'affiche dans la console de debug, c'est gagné
	while (1) {} // on "stop"
}

Pour tester cela, il faut allumer la Gameboy en premier pour être sur qu’elle attende le paquet avant que l’Arduino ne l’envoie.

Je reviendrai plus tard sur le but de cette bidouille, avec une démo je l’espère (comme ça, ça met la pression à la personne à l’initiative du projet ^^).

Si vous n’êtes pas membres wefrag, vos commentaires peuvent être recueillis par mail :

contact

Dans mes précédentes pérégrinations sur l’émulation, je vous expliquais à quel point j’étais épaté par les émulateurs modernes tels que Dolphin, PCSX2, nullDC, etc. En effet, ces émulateurs ont une approche particulière pour émuler une machine : ils recompilent les programmes à la volée. Enfin, pas tout à fait, mais on appelle communément cette méthode la recompilation dynamique (abrégée dynarec en anglais), ou parfois compilation just in time (JIT). Quel est l’intérêt de cette méthode ? C’est ultra performant.

Je vous propose donc d’explorer la recompilation dynamique dans ce billet et de comparer son fonctionnement à la méthode plus classique de l’interpréteur. Mais avant toutes choses, un peu de théorie sur l’émulation.

Les figures étant plutôt larges, je vous recommande une lecture sur une colonne, si vous n’êtes pas déjà dessus.

Pourquoi et comment émuler ?

Dans le fabuleux monde des processeurs, il existe une myriade de familles d’architectures, comme par exemple les suivantes :

  • x86 (les CPU Intel et AMD qui équipent vos PC/Mac)
  • PowerPC (toutes les consoles de salon de la génération actuelle)
  • ARM (dans tous vos smartphones et consoles portables)
  • Et bien d’autres plus obscures/obsolètes (MIPS, Alpha, Z80, 68000, SPARC…)

Chacune de ces architectures a une structure différente et notamment des instructions différentes qui sont parfois codées d’une façon différente. Cela fait beaucoup d’apparition du mot “différent” dans une seule phrase et c’est justement là le problème : un programme conçu pour une architecture donnée, ne pourra absolument pas être compris par une autre.

Intervient alors l’émulation, qui consiste à faire tourner un programme d’une architecture source sur une architecture cible, à travers un programme (l’émulateur) qui tourne sur l’architecture cible. Par définition, un émulateur “simule” ou “reproduit à l’identique” le comportement d’une architecture source.

Dans le domaine de l’émulation, il existe deux grandes approches :

  • L’interprétation, qui consiste à interpréter un programme source indépendamment de la cible;
  • La recompilation dynamique, qui consiste à transcrire un programme source en son équivalent cible.

Mais tout d’abord, qu’est-ce qu’un programme, et que fait un processeur ?

Un programme est une liste d’instructions à exécuter par le processeur. Ces instructions sont codées suivant un format propre à l’architecture pour laquelle le programme a été conçu. Le job d’un processeur est alors de décoder les instructions, et de les exécuter. Ci-dessous, un schéma très simplifié pour résumer.

emu1

Interpréter un programme

Un interpréteur consiste en un programme qui va simuler à haut niveau le comportement électronique d’un processeur. C’est-à-dire que si un processeur source a une instruction câblée qui fait “A = A + B”, alors l’émulateur possèdera une fonction (faites en C par exemple) qui simule cette opération. L’émulateur est ensuite compilé en langage machine pour l’architecture cible. Ci-dessous, un beau schéma.

emu2

La recompilation dynamique

L’autre méthode part d’un principe assez bête. Si toutes les architectures processeur ont bien des jeux d’instructions différents, la plupart des instructions ont des instructions (ou combinaisons d’instructions) équivalentes d’une architecture à l’autre.

Exemple

Le code binaire PowerPC “1C86000A” est décodé par l’instruction “mulli 4, 6, 10″, une simple opération de multiplication entre registres (r4 = r6 * 10).

Or, il existe en x86 une instruction équivalente, “imul 10″ (qui fait AX = AL * 10) et qui sera codée par le code binaire “F60A” (la longueur d’un code binaire n’a rien à voir avec sa performance; sa taille dépend de l’architecture).

Faire de la recompilation dynamique consiste alors à exécuter “F60A” dès lors que l’émulateur rencontrera “1C86000A”, comme décrit ci-après. La plupart du temps, on passe par un buffer d’instructions que l’on exécutera d’un seul tenant au moment opportun (par souci de performance et pour réserver la mémoire d’échange).

emu3

Concrètement, comment fait-on et où est la différence ?

Prenons un exemple d’itération d’un émulateur et comparons les deux approches. En entrée, notre émulateur reçoit le code PowerPC “1C86000A” (décrit précédemment). Le tableau ci-dessous détailles les différences.

dynarec

J’ai pris quelques raccourcis pour illustrer les différences (notamment masqué les overhead générés), mais je pense que l’essentiel est souligné: les performances. Parmi les raccourcis que j’ai pris, il y a toute une gymnastique de traitement des registres. En effet, l’architecture PowerPC possède quelques 64 registres alors que x86 en possède 2 à 4 fois moins suivant les versions. Il faut donc assurer la cohérence. Il faut aussi récupérer les résultats des opérations et les attribuer aux bons registres. Bref, la correspondance 1:1 est un cas idéal.

On peut aussi effectuer des optimisations. Parfois, une suite bien précise d’instructions peut être factorisée par une seule autre instruction (par exemple SSE 2/3/4). Un émulateur qui irait encore plus loin examinerait le code généré dans le buffer et chercherait des bouts à factoriser.

Il est aussi possible d’identifier des bouts de code récurrents issus des SDK. Par exemple, on peut connaître les instructions que génèrent certains SDK pour certaines fonctions et si on détecte ce pattern d’instructions, au lieu de chercher à le traduire, on exécute un code pré-fait optimisé. C’est très efficace sur les opérations fréquentes que font les SDK (typiquement, les opérations sur les string et autres lib helper).

Pour résumer, les avantages et inconvénients des deux approches

Le débat se situe essentiellement entre facilité-lenteur VS difficulté-performance.

Interpréteur

Avantages :

  • Indépendant de la cible, donc facilement portable;
  • Plus facile à débugger.

Inconvénient :

  • Lent.

Recompilation dynamique

Avantage :

  • Performant.

Inconvénients :

  • Devoir connaître sur le bout des doigts l’architecture source ET l’architecture cible;
  • Difficile à débugger;
  • Pas portable vers une autre cible sans créer un traducteur vers cette cible (et donc de connaître la nouvelle architecture cible sur le bout des doigts également);
  • Nécessite d’être programmé dans un langage qui permet de générer et d’exécuter des buffers d’instructions bas niveau.

Voilà pour la théorie / vulgarisation (en espérant que les puristes n’ai pas trop eu les poils qui se sont hérissés devant tant de raccourcis).

Si vous n’êtes pas membres wefrag, vos commentaires peuvent être recueillis par mail :

contact

Dans nos précédentes aventures culinaires, je vous expliquais les bases de l’émulation en vous proposant de programmer votre propre émulateur Chip8. Aujourd’hui, on va continuer cet émulateur en lui ajoutant le support du Super Chip8, une extension HD ! (Si ce n’est 4K.)

schip8emu

Pour ce tutoriel, nous allons reprendre là où nous nous étions arrêté. Nous avions terminé notre émulateur Chip8, dont vous pouvez télécharger les sources par ici (sous Creative Commons by-nc-sa).

Je vous conseil également de lire cet article sur une colonne pour une meilleure lisibilité du code.

Comme nous l’avions vu, Chip8 a été étendu pour être plus performant avec deux extensions : Super Chip8 et Mega Chip8. Dans ce billet, je vous propose d’implémenter les instructions Super Chip8. Pour mémoire, voici le tableau récapitulatif des apports de chaque extension :

tableauSuper Chip8 a été conçu pour être rétrocompatible avec Chip8. Comprendre par là qu’un programme Chip8 fonctionnera tout à fait normalement sur Super Chip8. Pour cela, Super Chip8 conserve toutes les instructions de son ainé sans y toucher. Il ne fait que rajouter de nouvelles instructions. Il ajoute également un mode étendu, qui active la résolution de 128×64 et change le comportement de certaines instructions pour qu’elles fonctionnent dans ce mode. Voici les instructions dédiées à Super Chip8 :

schip8

Sans plus attendre, taillons dans le gras !

Mise à l’échelle de l’image

Dans mon premier billet, j’avais avancé cela :

Ce petit émulateur a quelques défauts évidemment, comme la sortie vidéo qui scale plutôt mal. WPF faisant un filtre linéaire sur les textures, l’image est floue si on agrandi la fenêtre. Une solution est de se tourner vers une API graphique plus paramétrable, type DirectX/XNA, OpenGL, etc. Ou d’écrire plus de pixels manuellement lorsque l’on copie le frame buffer (mais c’est cradoc). Ou encore d’utiliser un filtre, comme SuperEagle, HQ2X… que la plupart des émulateurs populaires utilisent.

Ce qui reste vrai. Notre émulateur est un peu dégueulasse et ne rend pas hommage aux pixels bien carrés de Chip8. Mais, mais, mais… A ce moment, j’avais complètement omis le fait qu’il est possible de spécifier à WPF la manière dont il met à l’échelle les images. Par défaut, il utilise un filtre linéaire très rudimentaire. Ce que nous cherchons, c’est d’aller au plus simple et de ne pas appliquer de filtre (on parle de mise à l’échelle “au voisin le plus proche”). Pour cela, dans le constructeur de notre fenêtre WPF, nous allons rajouter le bout de code en gras :

            InitializeComponent();
            RenderOptions.SetBitmapScalingMode(frameBufferImage, BitmapScalingMode.NearestNeighbor);
            frameBufferImage.Source = writeableBitmap;

Et tada ! Nous avons nos bon gros pixels. Pour ceux qui souhaiteraient aller plus loin, l’article wikipédia sur la mise à l’échelle d’images est plutôt complet et présente tous les algorithmes utilisés dans les émulateurs (SuperEagle, 2xSal, HQ2X…). La plupart d’entre eux sont vraiment simples à mettre en place.

Le mode étendu

Deux instructions sont dédiées à la gestion du mode étendu. 00FF l’active alors que 00FE le désactive. Lorsqu’on l’active, il faut modifier le frame buffer pour une résolution de 128×64. Inversement, il faut le remettre à 64×32 quand on le désactive. Il va aussi nous falloir un booléen qui nous servira par ci par là.

        public bool ExtendedMode = false;

        // Désactive le mode étendu
        private void Inst_00FE()
        {
            ExtendedMode = false;
            _ScreenBuffer = new byte[8192]; // 64x32x4
        }

        // Active le mode étendu
        private void Inst_00FF()
        {
            ExtendedMode = true;
            _ScreenBuffer = new byte[32768]; // 128x64x4
        }

C’est un peu abrupte de recréer des tableaux ainsi, et potentiellement une source de gouffre à mémoire, mais dans le cadre de ce tutoriel, on ne va pas faire dans la dentelle. De plus, ces instructions sont très rarement appelées (une fois par jeu) et .NET gère bien les types valeurs, donc l’impact est vraiment nul. On aurait pu s’y prendre autrement (par ex. avoir un seul grand tableau ou un buffer par mode), mais faire ainsi nous simplifie la vie puisque l’on aura peu de modification à faire.

Défilement

Super Chip8 introduit trois instructions pour faire du défilement vertical et horizontal. Ces instructions ont deux particularités : elles ne wrap pas l’affichage (c’est-à-dire que le screen buffer n’est pas cyclique et s’éfface si déplacé) et le nombre de pixels déplacés est divisé par deux si le mode étendu n’est pas activé.

        // Défile de K pixels vers le bas, ou de K/2 en mode non étendu
        private void Inst_00CK()
        {
            int pixelToMove = 0; // Nombre de pixel à déplacer
            // On divise par 2 si le mode étendu est désactivé
            if (ExtendedMode)
                pixelToMove = _K * 512; // 512 bytes par lignes
            else
                pixelToMove = (int)Math.Ceiling(_K / 2.0f) * 256;

            // Position des pixels à déplacer
            int startPos = _ScreenBuffer.Length - pixelToMove - 1;

            for (int i = _ScreenBuffer.Length - 1; i >= 0; i--)
            {
                // Si on dépasse, on efface
                if (startPos >= 0)
                    _ScreenBuffer[i] = _ScreenBuffer[startPos];
                else
                    _ScreenBuffer[i] = 0;
                startPos--;
            }
        }

        // Défile de 4 pixels à droite, 2 si non étendu
        private void Inst_00FB()
        {
            // Nombre de byte à décaler à droite
            int offset = (ExtendedMode ? 16 : 8);

            for (int y = 0; y < (ExtendedMode ? 64 : 32); y++)
            {
                for (int x = (ExtendedMode ? 495 : 247); x >= 0; x--)
                {
                    // On décale à droite
                    int current = y * (ExtendedMode ? 512 : 256) + x;
                    _ScreenBuffer[current + offset] = _ScreenBuffer[current];
                    // On efface les 4 pixels à gauche
                    if (x < offset)
                        _ScreenBuffer[current] = 0;
                }
            }
        }

        // Défile de 4 pixels à gauche, 2 si non étendu
        private void Inst_00FC()
        {
            // Nombre de byte à décaler à droite
            int offset = (ExtendedMode ? 16 : 8);

            for (int y = 0; y < (ExtendedMode ? 64 : 32); y++)
            {
                for (int x = offset; x < (ExtendedMode ? 512 : 256); x++)
                {
                    // On décale à gauche
                    int current = y * (ExtendedMode ? 512 : 256) + x;
                    _ScreenBuffer[current - offset] = _ScreenBuffer[current];
                    // On efface à droite
                    if (x > (ExtendedMode ? 495 : 247))
                        _ScreenBuffer[current] = 0;
                }
            }
        }

Registres RPL

Deux nouvelles instructions ont une déscription un peu étrange. FX75 et FX85 évoque des registres RPL que Chip8 n’a pas. Quésaco ? RPL, ou ROM-based Procedural Language était le petit nom de l’environnement des calculatrices HP dans les années 80. Les deux instructions sus-citées permettent d’aller taper dans les registres du système d’exploitation. Leur utilité est double : avoir huit registres de plus où stocker des choses, et permettre à des programmes Super Chip8 de communiquer avec d’autres  (qu’ils soient Chip8 ou non). Ce dernier usage ne nous intéressent pas vraiment étant donné que notre émulateur ne tourne pas au sein d’un système. Par contre, avoir huit registres de plus, c’est chic. Pour le reste, ces instructions fonctionnent de façon quasi-similaire aux instructions FX55 et FX65 de Chip8 :

        // Registres RPL
        byte[] _RPL = new byte[8];

        // Stock V0 à VX dans les registres RPL, avec une limite à V7
        private void Inst_FX75()
        {
            int x = Math.Max(7, (int)_X);
            for (int i = 0; i <= x; i++)
                _RPL[i] = _V[i];
        }

        // Charge les registres RPL dans V0 à VX, avec une limite à V7
        private void Inst_FX85()
        {
            int x = Math.Max(7, (int)_X);
            for (int i = 0; i <= x; i++)
                _V[i] = _RPL[i];
        }

Table de caractères 10bit

Pour rappel, Chip8 utilise une table de sprites (stockée dans la partie privée de la mémoire) pour permettre l’affichage de caractères (de 0 à 9). Dans Chip8, ces sprites sont codés sur 5 octets, soit des sprites de 8×5 (dont 4×5 utiles en fait). Pour pouvoir afficher des caractères plus grands en mode étendu, Super Chip8 introduit une seconde table,  avec des caractères codés sur 10 octets (pour des sprites de 8×10 donc). Pour pointer sur cette table, il y a une nouvelle instruction :

        // Pointe I sur le digit de la table de caractères 10bit
        private void Inst_FX30()
        {
            _I = (ushort)(_V[_X] * 10 + 60);
        }

Et la table en question, que j’ai choisi de mettre à l’adresse 0×050h, soit directement après la table initiale (et toujours dans la mémoire privée) :

             // Table de caractères 10bit
            _memory[60] = 0x3C; _memory[61] = 0x7E; _memory[62] = 0xC3; _memory[63] = 0xC3; _memory[64] = 0xC3;
            _memory[65] = 0xC3; _memory[66] = 0xC3; _memory[67] = 0xC3; _memory[68] = 0x7E; _memory[69] = 0x3C;
            _memory[70] = 0x18; _memory[71] = 0x38; _memory[72] = 0x58; _memory[73] = 0x18; _memory[74] = 0x18;
            _memory[75] = 0x18; _memory[76] = 0x18; _memory[77] = 0x18; _memory[78] = 0x18; _memory[79] = 0x3C;
            _memory[80] = 0x3E; _memory[81] = 0x7F; _memory[82] = 0xC3; _memory[83] = 0x06; _memory[84] = 0x0C;
            _memory[85] = 0x18; _memory[86] = 0x30; _memory[87] = 0x60; _memory[88] = 0xFF; _memory[89] = 0xFF;
            _memory[90] = 0x3C; _memory[91] = 0x7E; _memory[92] = 0xC3; _memory[93] = 0x03; _memory[94] = 0x0E;
            _memory[95] = 0x0E; _memory[96] = 0x03; _memory[97] = 0xC3; _memory[98] = 0x7E; _memory[99] = 0x3C;
            _memory[100] = 0x06; _memory[101] = 0x0E; _memory[102] = 0x1E; _memory[103] = 0x36; _memory[104] = 0x66;
            _memory[105] = 0xC6; _memory[106] = 0xFF; _memory[107] = 0xFF; _memory[108] = 0x06; _memory[109] = 0x06;
            _memory[110] = 0xFF; _memory[111] = 0xFF; _memory[112] = 0xC0; _memory[113] = 0xC0; _memory[114] = 0xFC;
            _memory[115] = 0xFE; _memory[116] = 0x03; _memory[117] = 0xC3; _memory[118] = 0x7E; _memory[119] = 0x3C;
            _memory[120] = 0x3E; _memory[121] = 0x7C; _memory[122] = 0xC0; _memory[123] = 0xC0; _memory[124] = 0xFC;
            _memory[125] = 0xFE; _memory[126] = 0xC3; _memory[127] = 0xC3; _memory[128] = 0x7E; _memory[129] = 0x3C;
            _memory[130] = 0xFF; _memory[131] = 0xFF; _memory[132] = 0x03; _memory[133] = 0x06; _memory[134] = 0x0C;
            _memory[135] = 0x18; _memory[136] = 0x30; _memory[137] = 0x60; _memory[138] = 0x60; _memory[139] = 0x60;
            _memory[140] = 0x3C; _memory[141] = 0x7E; _memory[142] = 0xC3; _memory[143] = 0xC3; _memory[144] = 0x7E;
            _memory[145] = 0x7E; _memory[146] = 0xC3; _memory[147] = 0xC3; _memory[148] = 0x7E; _memory[149] = 0x3C;
            _memory[150] = 0x3C; _memory[151] = 0x7E; _memory[152] = 0xC3; _memory[153] = 0xC3; _memory[154] = 0x7F;
            _memory[155] = 0x3F; _memory[156] = 0x03; _memory[157] = 0x03; _memory[158] = 0x3E; _memory[159] = 0x7C;

Sprites 16×16 et 8×16

Dans Chip8, l’affichage de sprites se fait via l’instruction DXYK, où K détermine le nombre de lignes que doit faire le sprite, avec K = 1 à F comme valeurs possibles (soit des sprites allant de 8×1 à 8×15). Super Chip8 autorise K = 0, qui va signifier un sprite de 8×16. Si le mode étendu est activé et que K = 0, les sprites peuvent faire 16×16 pixels. Dans tous les autres cas, le comportement de l’instruction est le même que pour Chip8 (même si le mode étendu est actif, K = 3 donnera des sprites de 8×3). En gras, ce qui a été ajouté/modifié.

        private void Inst_DXYK()
        {
            // On reset le flag
            _V[0xF] = 0;
            // Gestion de Super Chip8
            if (_K == 0)
                _K = 16;
            // Pour chaque ligne de pixels à écrire
            for (int i = 0; i < _K; i++)
            {
                byte ligne = _memory[_I + i];
                // Gestion de Super Chip8
                byte ligne2 = 0;
                int pixelsInLine = 8;
                // Cas particulier 16x16
                if (_K == 16 && ExtendedMode)
                {
                    ligne = _memory[_I + i * 2];
                    ligne2 = _memory[_I + i * 2 + 1];
                    pixelsInLine = 16;
                }
                // Pour chaque pixel dans la ligne
                for (int n = 0; n < pixelsInLine; n++)
                {
                    // On extrait le pixel à écrire
                    byte pixel = (byte)((byte)(ligne << n) >> 7);
                    if (n >= 8)
                    {
                        int n2 = n - 8;
                        pixel = (byte)((byte)(ligne2 << n2) >> 7);
                    }
                    // On calcul l'adresse du pixel dans le frame buffer
                    int pixelAddress = _V[_X] + n + (_V[_Y] + i) * (ExtendedMode ? 128 : 64);

                    // Le reste de la méthode est inchangé...

Mise à jour du dispatcher

Il faut bien évidemment mettre à jour notre dispatcher pour qu’il tienne compte de ces nouvelles instructions (en gras, du code en plus) :

            switch (_Code)
            {
                case 0x0000:
                    if (_Y == 0xC)
                        Inst_00CK();
                    else
                        switch (_KK)
                        {
                            case 0xE0: Inst_00E0(); break;
                            case 0xEE: Inst_00EE(); break;
                            case 0xFB: Inst_00FB(); break;
                            case 0xFC: Inst_00FC(); break;
                            case 0xFE: Inst_00FE(); break;
                            case 0xFF: Inst_00FF(); break;
                        }
                    break;

Puis un peu plus bas :

                case 0xF000:
                switch (_KK)
                    {
                        case 0x07: Inst_FX07(); break;
                        case 0x0A: Inst_FX0A(); break;
                        case 0x15: Inst_FX15(); break;
                        case 0x18: Inst_FX18(); break;
                        case 0x1E: Inst_FX1E(); break;
                        case 0x29: Inst_FX29(); break;
                        case 0x30: Inst_FX30(); break;
                        case 0x33: Inst_FX33(); break;
                        case 0x55: Inst_FX55(); break;
                        case 0x65: Inst_FX65(); break;
                        case 0x75: Inst_FX75(); break;
                        case 0x85: Inst_FX85(); break;
                    }
                break;
            }

Mise à jour de WPF

Enfin, il nous faut un peu modifier l’affichage du screen buffer pour gérer le mode étendu (avec les changements, toujours en gras) :

        bool ExtendedMode = false;

        private void Render(object sender, EventArgs e)
        {
            byte[] data = Chip8Emulator.GetScreenBuffer();

            if (!ExtendedMode && Chip8Emulator.ExtendedMode)
            {
                writeableBitmap = new WriteableBitmap(128, 64, 60, 60, PixelFormats.Bgra32, null);
                frameBufferImage.Source = writeableBitmap;
                ExtendedMode = true;
            }
            else if (ExtendedMode && !Chip8Emulator.ExtendedMode)
            {
                writeableBitmap = new WriteableBitmap(64, 32, 60, 60, PixelFormats.Bgra32, null);
                frameBufferImage.Source = writeableBitmap;
                ExtendedMode = false;
            }

            if (data != null)
            {
                // write our pixels into the bitmap source
                writeableBitmap.Lock();
                Marshal.Copy(data, 0, writeableBitmap.BackBuffer, data.Length);
                writeableBitmap.AddDirtyRect(
                    new Int32Rect(0, 0, (ExtendedMode ? 128 : 64), (ExtendedMode ? 64 : 32)));
                writeableBitmap.Unlock();
            }
        }

Et voilà, nous avons terminé l’ajout des extensions Super Chip8 à notre émulateur. Pour le tester, je vous rappel que vous pouvez télécharger un pack de ROM publiques par ici.

Comme la dernière fois, le code source de ce billet est mis à disposition, sous licence Creative Commons by-nc-sa.

Au prochain épisode, nous attequerons les extensions Mega Chip8 avec notamment la gestion du son en PCM et des palettes de couleurs.

Si vous n’êtes pas membres wefrag, vos commentaires peuvent être recueillis par mail :

contact