February to April Gears emulator update
Fixing interrupts and starting audio !
May 02, 2023
Previously, I had a bug in my gears emulator, which I then fixed. But Sonic 2 still wasn't working, so let's see what went wrong there.
Missing interrupt behaviour
The Sonic 2 ROM wasn't starting at all, leaving an uninteresting black screen, which led me to suspect some kind of infinite loop. I said in the previous posts that I thought this loop could have been related to timings or interrupts.
So I looked at how the game behaved in Emulicious, another (closed-source) emulator with great debugging capability. And I noticed something weird on my side of the emulation: after a HALT
instruction (that shuts down the CPU similarly to running an infinite number of NOPs), the VDP interrupt would wake-up the CPU, it would be handled and return. But when returning it jumped straight into the previous HALT
instruction (!), meaning it looped and never moved forward. I fixed this by implementing the missing PC increment, and it set forward the startup of Sonic 2.
Later, I went looking at another game that didn't finish starting: Global Gladiators. I figured it might be something related with an infinite loop somewhere + interrupts. I went to the frame that froze...
... and I noticed something similar: a loop reading from the VDP V Counter (or line counter), waiting for it to be 0xC0
. But it never happened ! Because at 0xC0
, the VDP raised an interrupt and by the time the interrupt was handled, the VDP already had processed a few other lines. So the code looped, waiting for an event that never happened. Emulicious seemed to send the interrupt at 0xC1
, contrary to what the official Game Gear manual said would happen; but Charles McDonald's VDP documentation from 2002 said it should happen 0xC1
. So I simply fixed it by using this same value. But something tells me there might be other timing-related bugs hiding here. At least I can now play on of the greatest games of the platform 😉
How about this ? |
Did a brand pay for this ? |
In the end, this untested interrupt code is the source of multiple bugs... I might need to find a way to write simple test for these.
Palettes
This one is a quite simple and well-documented VDP feature for background patterns: there's a bit to have them use the second color palette (usually for sprites). It allows using 16 more colors for the background: background can use 32 colors, while sprites can only use the second half (16) of those colors at a given time.
Bad rendering: which character is selected ? |
Palette bit is used to implement character selection |
The color palette select bit was already decoded, but not used for rendering, so the fix was straightforward.
Bad rendering with palette 0 |
Sonic and Tails use pallete 1 |
Making sounds
After fixing enough VDP bugs that many games are now playable, I started looking into making sound. I used the rust crate cpal which seemed like the most well-maintained and portable for abstraction for playing audio. After a PoC to play a simple 440 Hz sinusoid, I slowly wired things up. The programming model of cpal is to provide a callback that will be polled by an audio thread, so it forced my design to do more Send+Sync
data structures, while initially everything in gears was single threaded, with the use of Rc
for some simple high-level things like dynamic device registration. It forced me to use Arc
and Mutex
to ensure the PSG data structures would be shareable across threads.
I was inspired by the design of the VGM 'audio' files, which are just basically command dumps from the PSG writes. Audio rendering is done as late as possible to ensure being as independent as possible from the sample format. The queue of commands updates the PSG internal state, and it generates audio when cycles go forward.
Just like with the VDP, the CPU cycles are the reference, so instruction cycles need to be correctly counted or emulation can be visibly changed.
Audio is played in real time, so there is a high accuracy component to ensure what comes out sounds 'good'; and in gears, synchronization is far from done — not because of instructions: cycle accuracy is good enough to pass the fuse test suite. But because the emulator currently relies on VSYNC to do 60 frames per second: it's very convenient that all my screens are also at 60Hz, like the Game Gear's LCD!
But since frames during which the VDP is BLANK (screen "off") are not displayed, the emulation goes too fast during blank frames ! It means that PSG commands will accumulate and need to be "flushed" later on to keep the queue from growing indefinitely; this leads to weird fast music effects, then going back to normal speed, like during the beginning of the Green Level in Sonic 1:
As you can hear, in addition to proper synchronization, there are other things missing, like noise generation. Maybe for the next update ?