gameboy-sharp

Un emulatore Game Boy (DMG) in C#. Un core senza dipendenze dal frontend, così l'intera macchina funziona ed è verificata headless su ROM di test standard.

gameboy-sharp
TL;DR

Un emulatore di Game Boy (modello DMG) scritto in C#. Il core è separato dal frontend e verificato headless contro ROM di test standard, così i giochi altrui girano bit per bit.

Panoramica

gameboy-sharp è un emulatore del Game Boy originale, il modello DMG, scritto in C#. Il compito sembra semplice: far girare giochi usciti trent'anni fa senza cambiare un solo byte. La difficoltà si nasconde nella parola "fedelmente" - l'emulatore deve imitare l'hardware con precisione tale che il codice originale non si accorga di nulla.

Abbiamo costruito il core in modo che non sappia nulla di una finestra né del suono. Tutta la macchina è una libreria pura che restituisce un buffer di immagine e campioni audio, e solo il frontend li trasforma in ciò che vedi e senti. È questo che permette di far girare tutta la console senza schermo e confrontarne il risultato con ciò che dovrebbe uscire.

I bug in un emulatore quasi mai vanno in crash con fragore. Il gioco parte, il gioco gira - solo il suono è un filo troppo alto, o il personaggio si sposta di un pixel di troppo. Non c'è eccezione, non c'è crash, solo un comportamento sottilmente sbagliato che "a occhio" non distingui da quello corretto.

Questo cambia tutto il modo di lavorare. Non puoi fidarti dell'impressione che "sembra a posto", perché è proprio quell'impressione a mentire più spesso. L'unica cosa di cui ci si può fidare è il confronto dello stato della macchina con un valore noto in anticipo - ed è attorno a questo che è costruito tutto il progetto.

Il cuore della macchina: il processore

Il cuore della console è il processore del Game Boy, una variante dello Z80. Ogni istruzione è la lettura di un byte di opcode e l'esecuzione esatta di ciò che farebbe l'originale, insieme al numero di cicli che quell'istruzione costa. Il timing non è un dettaglio - è lui a decidere se suono e immagine sono sincronizzati come sull'hardware reale.

Cpu.cs · csharp
byte opcode = ReadByte(PC++);

switch (opcode) {
    case 0x00: cycles += 4; break;
    case 0x3E: A = ReadByte(PC++); cycles += 8; break;
    case 0xC3: PC = ReadWord(); cycles += 16; break;
    default: cycles += Execute(opcode); break;
}

Un'istruzione e il suo costo in cicli

OpcodeIstruzioneCicli
0x00NOP4
0x3ELD A, d88
0xC3JP a1616
i
Nota

Il core non sa nulla di una finestra né del suono - è una macchina pura. Il frontend riceve solo un buffer di immagine e campioni audio. È questo che permette di far girare tutta la console senza schermo e confrontare il risultato con quello atteso, ciclo per ciclo.

Test al ciclo esatto

La comunità dell'emulazione mantiene set di ROM di test che verificano singoli registri, flag e il timing al ciclo. Le eseguiamo headless e confrontiamo lo stato della macchina con ciò che dovrebbe uscire. È l'unico modo onesto per dire che un emulatore è corretto, e non solo che "funziona da me".

È proprio per questo che il core è stato tagliato via dal frontend. Se la macchina avesse bisogno di una finestra, ogni test andrebbe cliccato a mano. Dato che è una libreria pura, le ROM di test scorrono in automatico, e il verdetto è binario: coincide oppure no.

Il risultato: i giochi altrui bit per bit

Ne esce un emulatore che fa girare i giochi altrui e supera le ROM di test standard, e non solo uno che mostra il logo Nintendo e si ferma. Il suono è dove deve stare, il personaggio si sposta del numero di pixel che deve, e la correttezza è provata dai test, non da un'impressione. È un progetto nato dall'amore per il vecchio hardware e portato a uno stato in cui questo si vede.

Altri progetti

Altri lavori della stessa categoria - scopri come affrontiamo sfide simili.

Hai un progetto simile?

Contattaci - il preventivo è gratuito e arriva entro un'ora.