Reviving a 19 Year Old Gameboy Emulator: Difference between revisions

no edit summary
No edit summary
No edit summary
Line 13: Line 13:


It looks like there was an attempt at this already in the code, using the Win32 <code>QueryPerformanceCounter</code> API, called <code>RealSpeedEmulation</code>. Unfortunately it calls <code>QueryPerformanceCounter</code> somehow completely wrong, passing in a member of a record rather than a pointer to that record... No idea why. I just ditched <code>RealSpeedEmulation</code> and did what [https://github.com/Dooskington/GameLad GameLad] does, [https://github.com/SuperDisk/UGE/commit/2caeeb20ac487df4fe4bc2352b9da0ed51eee1b0 which is just spin for a period of time until we need more cycles to be emulated.]
It looks like there was an attempt at this already in the code, using the Win32 <code>QueryPerformanceCounter</code> API, called <code>RealSpeedEmulation</code>. Unfortunately it calls <code>QueryPerformanceCounter</code> somehow completely wrong, passing in a member of a record rather than a pointer to that record... No idea why. I just ditched <code>RealSpeedEmulation</code> and did what [https://github.com/Dooskington/GameLad GameLad] does, [https://github.com/SuperDisk/UGE/commit/2caeeb20ac487df4fe4bc2352b9da0ed51eee1b0 which is just spin for a period of time until we need more cycles to be emulated.]
<pre>
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;
</pre>


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.
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.
Line 20: Line 42:
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 <code>dib_out.pas</code> I changed <code>biheight := 144;</code> to <code>biheight := -144;</code> and....
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 <code>dib_out.pas</code> I changed <code>biheight := 144;</code> to <code>biheight := -144;</code> and....


Nice. We've eliminated the
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 =
 
<pre>
var
  cs: TRTLCriticalSection;
  bufs: array[0..1] of THANDLE;
  bufPtr: array[0..1] of pointer;
  bufHdr: array[0..1] of THANDLE;
</pre>
 
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 <code>BASS</code> 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.
 
[[File:Curious.PNG]]
 
<pre>
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;
</pre>
 
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 <code>vol</code> to be of type <code>ShortInt</code> 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.
<pre>
l := shortint(l shr 2) + 128;
r := shortint(r shr 2) + 128;
if (l <> 170) and (r <> 170) then
  SoundOutBits(l, r, cycles);
</pre>
 
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:
 
<pre>
if stage and 1 > 0 then
  snd[3].Bit := snd[3].Bit and $f
else
  snd[3].Bit := snd[2].Bit shr 4;
</pre>
 
Do you see it? It's accessing <code>snd[3].Bit</code> in the first branch of the <code>if</code>, but <code>snd[2]</code> in the second one. Let's just change it to 2, and...
 
Holy shit, it's perfect! And 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. Now that I'm done heroically fixing this emulator with my insane hacking skills, I'm off to work on the actual tracker now.
 
= PS =
 
I'd just like to point out how amazing it is that the FreePascal folks have recreated the Delphi environment so well that