10.122014

Grundlagen der Emulator Programmierung

pong Wer kennt nicht die Klassiker wie Space Invaders, Pong oder auch Tetris? Oft kopiert finden wir sie in Versionen für unsere Handys oder PCs und sogar Taschenrechner wieder. Heute wollen wir aber einen etwas anderen Weg einschlagen. Was ist nötig, um ein Spiel wie Pong auf einen modernen Browser zu starten. Der Clou ist aber, dass wir Pong nicht selbst schreiben sondern einen Emulator programmieren werden, der eine Vielzahl an Spielen ausführen kann. Zunächst erarbeiten wir uns gemeinsam die generelle Struktur eines "Emulators". Als zweiter Punkt steht dann die Umsetzung einzelner Befehle . Gegen Ende der Einführung sollte es kein Problem sein, einen eigenen Emulator zu schreiben und somit Eindruck schinden zu können. Um ein System zu emulieren müssen wir jeden Befehl und das durch ihn erzeugte Verhalten nachbilden. Hierzu ist umfassende Kenntnis des zu emulierenden Systems notwendig. Um den Einstieg möglichst einfach zu gestalten betrachten wir CHIP-8, eine virtuelle Maschine, die auch in einigen Taschenrechnern umgesetzt worden ist.

Auf zur Spezifikation der Chip-8-VM. Diese verfügt über:

  • Arbeitsspeicher (memory) mit insgesamt 0x1000 Byte
  • Programmzähler (pc), welcher auf den aktuell auszuführenden Befehl im Speicher zeigt
  • 16 Arbeitsregister (V0 - VF)  mit je 1 Byte Speicher
  • 1 Adressregister (I) mit 2 Byte Speicher
  • 2 Timer (delayTimer und soundTimer)
  • Callstack (pcStack) und Stackpointer (sp) zum Abarbeiten von Unterfunktionen (maximal 16).

Außerdem besitzt der Chip-8 für die Nutzer-Interaktion:

  • Ein monochromes Display mit 64x32 Pixeln
  • 16 Tasten, die zur Eingabe genutzt werden
  • Einen "Lautsprecher" der ein festes Signal erzeugen kann (beep)

Also legen wir erstmal unsere Datenstrukturen fest:

  // Aktueller Befehl
  var opcode;

  // Arbeitsspeicher
  var memory = new ArrayBuffer(0x10000);
  for (var i = 0; i < 4096; i++) {
    memory[i] = 0;
  }

  // Arbeitsregister
  var V = new ArrayBuffer(0xF);
  for (var i = 0; i < 16; i++) {
    V[i] = 0;
  }

  // Programmzähler
  var pc = 0x200;   

  // Adressregister
  var I = 0; 

  // Bildschirm
  var buffer = new ArrayBuffer(64 * 32);
  for (var i = (64 * 32); i > 0; --i) {
    buffer[i] = 0;
  }

  // Timer
  var delayTimer = 0;
  var soundTimer = 0;

  // Callstack
  var pcStack = new ArrayBuffer(0xF);

  // Stack Pointer
  var sp = 0;

  // Keyboard Status
  var keys = new ArrayBuffer(0xF);
  var V = new ArrayBuffer(0xF);
  for (var i = 0; i < 16; i++) {
    keys[i] = 0;
  }

Für die Verarbeitung ist wichtig zu wissen, dass der Chip-8 jeweils Befehle (opcodes) mit fester Länge von je 2 Byte verarbeitet. Um ein Programm auszuführen wird dieses im Speicher ab Adresse 0x200 abgelegt, der Programmzähler auf die Startadresse (0x200) gesetzt und dann kontinuierlich ein Befehl nach dem anderen aus dem Speicher geladen und ausgeführt. Für die meisten Befehle bedeutet das, dass der Programmzähler auf die nächste Speicherstelle gesetzt, also um 2 inkrementiert wird. Nach jedem Befehl werden noch die Timer aktualisiert. Eine grobe Struktur für einen solchen Arbeitszyklus könnte wie folgt aussehen:

var emulateCycle = function() {
 // Befehl holen
 opcode = (memory[pc] << 8 | memory[pc + 1]);
 // Programmzähler erhöhen
 pc += 2;
 // Befehl ausführen
 processOpcode(opcode);

 // gegebenenfalls das Bild neu zeichnen
 drawScreen();

 // die Timer runterzählen
 if (delayTimer > 0) {
   --delayTimer;
 }
 if (soundTimer > 0) {
   --soundTimer;
   if (soundTimer == 0) {
     // einen Ton erzeugen
   }
 }
};

Nun fehlt natürlich noch die Abarbeitung der einzelnen Opcodes (processOpcode). Zunächst müssen wir herausfinden, welchen der 35 Befehle wir gerade verarbeiten. Eine einfache Lösung ist hierbei zunächst nach den 4 höchstwertigsten Bits zu schauen und so eine Vorauswahl zu treffen. Schauen wir uns exemplarisch 3 Befehle an:

0xaNNN:

Dieser Befehl setzt das Adressregister I auf den Wert NNN. Hierbei steht NNN für die 12 niederwertigen Bits des Opcodes. Eine Übersetzung nach Javascript ist somit sehr einfach durchzuführen:

if ((opcode & 0xF000) == 0xA000) {
  I = opcode & 0x0FFF;
}

Deutlich komplexer ist dann schon der zum Zeichnen von "Grafiken" verwendete Befehl 0xdXYN:

Hierbei werden die folgenden N Byte beginnend ab I als binäre Grafik mit größe 8xN Pixeln interpretiert. Diese wird dann beginnend mit Position XxY auf den Bildschirm geschrieben. Pixel die außerhalb des Zeichenbereiches liegen werden ignoriert. Pixel werden hierbei mittels XOR-Operation verknüpft. Das bedeutet, dass aktive (weiße) Pixel beim überzeichnen gelöscht werden, wie im folgenden Schaubild zu sehen:

xor_drawing

Wurde wenigstens ein Pixel hierbei gelöscht, so wird das Register V0 auf 1 gesetzt, ansonsten auf 0.

0xfX29:

Für die Darstellung von Zahlen besitzt der Chip8 einen Zeichensatz von 16 Zahlsymbolen mit einer Auflösung von 4x5 Pixeln. Diese sind Teil des Regulären Arbeitsspeichers und können mittels des Befehls [0xFX29] genutzt werden. Dieser speichert die Anfangsadresse des Zahlsymbols für welches im Register VX steht in I, so dass diese direkt gezeichnet werden können.

Im Code könnte das ganze dann so aussehen:

if ((opcode & 0xF0FF) == 0xF029) {
  X = (opcode & 0x0F00) >>> 8;
  I = FONT_BASE + (V[X] * 0x5); // FONT_BASE == Adresse des ersten Schriftsymbols im Speicher
}
if ((opcode & 0xF000) == 0xD000) {
 var x = V[(opcode & 0xF00) >>> 8];
 var y = V[(opcode & 0x0F0) >>> 4];
 var height = opcode & 0xF;
 var pixel;
 V[0x0] = 0;
 for (var yline = 0; yline < height; ++yline) {
   pixel = memory[I + yline];
   for (var xline = 0; xline < 8; ++xline) {
     if ((pixel & (0x80 >>> xline)) != 0) {
     var position = (x + xline + ((y + yline) * 64));
     if (buffer[position] !== 0)
       V[0xF] = 1;
       buffer[position] ^= 1;
     }
   }
 }
}

Das Schreiben der 32 verbleibenden Befehle sowie die Umsetzung von Laderoutinen / der Tastatur überlasse euch. Für die Darstellung des emulierten Bildschirmes (buffer) bietet sich in HTM5 ein Canvas-Objekt an. Wer jetzt Lust bekommen hat sich selbst an einen Emulator zu setzen, der sollte unbedingt den folgenden Link anschauen: Mastering Chip-8 von Matthew Mikolay.