Reviving a 19 Year Old Gameboy Emulator

From Nick Faro's Homepage
Jump to navigation Jump to search

Back in 2014 I was messing around with the idea of converting Gameboy .GBS soundtracks into Amiga .MOD files for a game I was doing. If you don't know what a .GBS file is, it's basically just a Gameboy ROM with Z80 code but with the graphics routines and gameplay stripped out, so it's just the sound driver and music data, so in order to play it back you essentially have to emulate the full Gameboy processor and sound chip. At the time I was sort of infatuated with FreePascal and Lazarus, and wanted to find a Gameboy emulator written in Pascal that I could maybe yank the CPU and sound code from.

Somehow after scavenging over a bunch of old forum posts, I found (apparently) the only Gameboy emulator written in Pascal, ever: UGE (edit: it turns out the famous BGB emulator is also written in Pascal... Too bad it's not open source!). It was written by a guy named Christian Hackbart with Delphi and released in 2000, and the only available download was from a mirror on Zophar's Domain. I started it up, loaded a ROM, was met with some ear-piercing noise, and then the emulator crashed. I canned the project pretty shortly after and that was pretty much that.


Fast-forward 5 years and I had the idea for another project centered around the Gameboy's sound system, and was compelled to go dig up the old code again and maybe see if I could fix it-- after all, it might be a good learning exercise to fix this thing up, and I kinda like the idea of an old abandoned piece of software rising from the ashes. So I dug in and tried to find out why this bad boy is flipping its graphics upside down, belching out horrible sound, running insanely fast, and crashing.

Fixing the speed

By a stroke of luck, I figured out that the reason the emulator was crashing: the sound emulation. If you untick the sound box in the menu, it stops running the sound code and it can be played for more than 5 seconds before blowing chunks. It still runs insanely fast and the graphics are upside down though. I tackled the speed issue first... should be as simple as just throwing in a spinloop to waste time and lock it to 60fps or whatever.

It looks like there was an attempt at this already in the code, using the Win32 QueryPerformanceCounter API, called RealSpeedEmulation. Unfortunately it calls QueryPerformanceCounter somehow completely wrong, passing in a member of a record rather than a pointer to that record... No idea why. I just ditched RealSpeedEmulation and did what GameLad does, which is just spin for a period of time until we need more cycles to be emulated.

if f_stopped then
  while f_stopped do
	application.ProcessMessages;

while (cycles < CyclesPerFrame) do
	  cycles += z80_decode;

cycles -= CyclesPerFrame;

while True do begin
  QueryPerformanceCounter(@li);
  frameEnd := li.QuadPart;

  frameElapsedInSec := (frameEnd - frameStart) / tickFreq;
  if frameElapsedInSec > TimePerFrame then
	 Break;
end;

frameStart := frameEnd;

Bam! Already it's almost playable, and the sound isn't causing crashes. I think it was crashing before because the extreme speed of the emulation was causing a sound buffer overrun or something.

Fixing the graphics

The emulator actually does render the screen right-side-up when using DirectDraw, but I can't record that with OBS for some reason. It renders flipped graphics when drawing with GDI-- let's fix that. In dib_out.pas I changed biheight := 144; to biheight := -144; and....

Nice. We've eliminated the graphics problem, speed problem, and crashing problem with like 10 lines of code and a 1 character change. All that's really left now is the sound...

Fixing the sound

var
  cs: TRTLCriticalSection;
  bufs: array[0..1] of THANDLE;
  bufPtr: array[0..1] of pointer;
  bufHdr: array[0..1] of THANDLE;

The first thing I noticed is that the sound output is going through this convoluted 2-buffer system where it writes to one, and when it fills up, switch to the other, and then provides it to the Windows MME system or something-- I honestly can't really follow it, it's pretty spaghetti. I ripped out all MME stuff and added a call to write to a BASS stream whenever it would normally write to one of these buffers. Sound is still horrible, but at least it doesn't crash when the emulation goes too fast!

It was at this point I noticed something really curious.

Curious.PNG

var
  snd: array[1..4] of record
    // public:
    ChannelOFF: boolean; // (un)mute Channel
    // private:
    enable: boolean;
    Freq: integer;
    Vol: byte;
    Len: integer;
    swpCnt: byte;
    EnvCnt: byte;
    bit: byte;
    cnt: integer;
  end;

That's right, the volume value for each channel is being stored as an unsigned byte, so it will never go below zero! I changed vol to be of type ShortInt and....

Wow, still bad, but a lot better! Another huge gain from an 8 character change! What else can we do?

I noticed this little nugget as well. When the sample is a certain value, it just doesn't write anything.

l := shortint(l shr 2) + 128;
r := shortint(r shr 2) + 128;
if (l <> 170) and (r <> 170) then
   SoundOutBits(l, r, cycles);

I don't know why, so let's try just commenting it out.

WTF? Another huge improvement! We're at a big net negative on code written here, yet it just keeps getting better. What else can we do?

The only problem left here is that channel 3, the Gameboy noise channel, produces a weird waveform instead of what it should. I figured it's probably another sign error somewhere, but it's actually even better:

if stage and 1 > 0 then
   snd[3].Bit := snd[3].Bit and $f
else
   snd[3].Bit := snd[2].Bit shr 4;

Do you see it? It's accessing snd[3].Bit in the first branch of the if, but snd[2].Bit in the second one. Let's just change it to 3, and...

Holy shit, it's perfect! Well, the audio is clipping a little bit. But all I did was change like 20 characters! It really strikes me that all Christian Hackbart had to do back in 2000 was do 5 lines of fixes and he would have had a near perfect Gameboy emulator on his hands. Also let me just say that it's super impressive that not only does near 20-year-old code work unmodified on Windows 10, but also that it's compiled by a totally different compiler and totally rewritten GUI toolkit, almost with zero changes. My hat is off to the FreePascal folks.

Now that I'm done heroically fixing this emulator with my insane hacking skills, I'm off to work on the actual tracker now.

Source code for UGE is all here: https://github.com/superdisk/uge