This post covers a few Genesis emulation bugs that I ran into at various points, that I don’t think are substantial enough to justify their own posts (barring maybe the third one). The bugs that caused these are not particularly interesting - 2 were caused by me failing to thoroughly read existing documentation, and 1 was an accuracy shortcut that worked until it didn’t - but I think it’s an interesting way to explore some unintuitive hardware behaviors and how games depend on them.
VRAM Fill Quirks
The VDP’s DMA unit supports three modes: CPU memory to VRAM, VRAM fill, and VRAM-to-VRAM copy. All three are a bit quirky, but this specific issue relates to VRAM fill.
VRAM fill repeatedly writes a single byte to VRAM with a configurable address increment. The most common use case is to fill a continuous region of VRAM with the same value, e.g. to zero out VRAM at boot.
Games initiate VRAM fill mostly the same way as the other DMA modes, but fill doesn’t require setting the DMA source address, and instead you need to write the desired fill data to the VDP data port after you initiate the DMA. The fill does not begin running until this data write. As with other DMA modes, games configure the address increment by writing to VDP register #15 (data port address increment); for the common fill use case this will always be 1.
VRAM fill doesn’t halt the CPU like memory-to-VRAM DMA does, but writing to any of the VDP ports will mess with an in-progress fill, so games poll a DMA busy flag in the VDP status register (bit 1) if they need to wait for a fill to finish. Games could theoretically do some other work in parallel during this time, but often they’ll start polling immediately after starting the fill.
According to the official documentation, fill is byte-size and uses the high byte of the 16-bit word written to the data port…except for the very first write, which does a normal word-size write to VRAM. Huh, ok.
A naive emulation of VRAM fill might look like this, triggered upon the VDP data port write:
|
|
This (mostly) works fine for the common use case, but some games use VRAM fill in other ways:
Contra: Hard CorpsTracing the game’s VDP interactions shows it running VRAM fills with the address increment set to 2 or 4 instead of 1, and that it often uses VRAM fill to write to VRAM addresses in the $B800-$BBFF range, which is where the game stores its horizontal scroll table. The emulator is clearly not handling this correctly!
The Genesis VDP natively supports per-tile-row and per-line horizontal background scrolling using a scroll value table stored in VRAM so that games can perform parallax scrolling effects without needing to manually alter scroll values in between lines. Here, the game’s VRAM writes to the H scroll table don’t appear to be going through as intended, so the background isn’t scrolling at all. The changing graphics are the game rewriting parts of the background tile map that are supposed to be horizontally scrolled offscreen.
Let’s see what the official documentation says about how this should work:

This is not very clear, and it’s also wrong! Part of how this works has been public knowledge for decades (at least since 2000 based on Charles MacDonald’s documentation), but I missed it during my first attempt at this.
First, the initial word-size write is not actually part of the VRAM fill. The VDP data port write goes through normally as a standard 16-bit write to VRAM, and then the VRAM fill starts. This means that the first write does not decrement the DMA length.
Once the fill begins, for some reason all of its VRAM writes invert the lowest address bit, i.e. they write to address ^ 1 instead of address. This doesn’t matter much if the address increment is 1 but it matters a lot if the increment is, say, 2 or 4. I think this is probably caused by some implementation detail of how they made fill byte-size instead of word-size, because VRAM-to-VRAM copy DMA is also byte-size and it also inverts the lowest address line when accessing VRAM.
So, the fill logic changes to:
|
|
This fixes Contra (and a few other games that do similar things, e.g. Thunder Force IV). The writes were actually going through before, but they were going to the wrong byte of each 16-bit H scroll value (really 10-bit, the highest 6 bits are unused).
For a bonus VRAM fill quirk, it increments the DMA source address on every byte copied, even though the fill itself doesn’t do anything with the source address. I’m not aware of any games that depend on this, but test ROMs have verified the behavior on actual hardware by running a fill and then immediately running another type of DMA without touching the lowest 17 bits of the source address.
Instant DMA
Accurately emulating VDP DMA timing for each byte copied can be somewhat tricky and/or slow. One possible simplification is to perform the memory transfer all at once when the CPU initiates it, calculate roughly how long it should take, and then stall the CPU for that long (a number of games do depend on memory-to-VRAM DMAs stalling the CPU for some amount of time).
This is obviously inaccurate, but it actually works perfectly fine most of the time! Except when it doesn’t:
No, the text is not supposed to flicker
Unlike on SNES, the Genesis VDP allows games to freely write to VRAM during active display, just with far more limited bandwidth than during vertical blanking: you can only write 18 bytes per scanline instead of 204 (in H40/H320px mode, less in H32). This limit applies to both manual writes from the CPU (which will temporarily stall if it writes too rapidly during active display) and to VDP DMA.
On the title screen here, Gaiares renders the bottom chunk of the screen using the Genesis’ window layer, a non-scrollable background that can optionally replace 1 of the 2 proper background planes in a configurable region of the screen. It’s mainly meant for rendering status bars or other mostly-static elements, very similar to the Game Boy’s window layer.
Whenever it wants to toggle the “Press START button” text between visible and invisible, it first runs a DMA to fill the entire window tile map with pure black tiles, then it immediately runs a second DMA to populate the tile map with references to the text tiles. The first DMA is really not necessary, but whatever, it works as long as the VDP never renders the bottom 56 lines of the screen in between the two DMAs. …Which is exactly what it’s doing in the above recording.
The game relies on VDP DMA timing to prevent this from ever happening, possibly accidentally. It starts the first DMA during active display and relies on the 18-byte-per-line bandwidth limit to prevent the DMA from overwriting the bottom 7 rows of the window tile map before the VDP has rendered those lines. Then the second DMA finishes before the beginning of the next frame.
You don’t need super accurate timing to fix this particular game; progressing DMA in batches once per scanline is good enough. You just can’t do the copy all at once. (I have since implemented much more accurate DMA/FIFO timing than this.)
Reading is Hard
Games access each controller I/O port through a pair of memory-mapped registers, a control port (e.g. $A10009 for player 1) and a data port (e.g. $A10003 for player 1). Some documentation refers to these as CTRL and DATA, and I’m going to use those names from here on because “control port” looks too similar to “controller port”.
Each controller port has 9 physical pins: a Vcc pin (positive voltage), a ground pin, and 7 data pins labeled TH, TR, TL, and D3-D0. Games can write to the CTRL port to configure whether each data pin is an input from the controller (bit = 0) or an output to the controller (bit = 1), they can read from the DATA port to get the current level of each data pin, and they can write to the DATA port to set the level of each output pin.
Bit 7 6 5 4 3 2 1 0
Pin - TH TR TL D3 D2 D1 D0
CTRL/DATA bit layout
The official 3-button and 6-button gamepads are designed to be used with the TH pin set to output and all other pins set to input (CTRL value 0x40). The 3-button gamepad’s protocol is very simple: when TH is high (1) it sets the other 6 pin states to the d-pad directions and the B and C buttons, and when TH is low (0) it sets 2 of the pins to the A and Start buttons (and 2 of the others to d-pad up/down, they’re present in both for some reason).
All buttons and directions are active low, so 0 means pressed and 1 means not pressed. It’s extremely common for games to invert the bits when reading so that 1 means pressed.
Bit 5 4 3 2 1 0
Pin TR TL D3 D2 D1 D0
Button C B R L D U
TH=1 input bits
Bit 5 4 3 2 1 0
Pin TR TL D3 D2 D1 D0
Button S A 0 0 D U
TH=0 input bits
When TH=0, the D2 and D3 pins always read 0. Some games use this to detect whether there’s anything connected to the controller port, since they’ll read 1 if nothing is connected.
Games typically write to CTRL at boot and then never touch it again, then once per frame or so they’ll read inputs by doing the following (for 3-button):
- Write to DATA to set TH=0
- Read from DATA (A, Start)
- Write to DATA to set TH=1
- Read from DATA (D-Pad directions, B, C)
You’re supposed to wait for a few cycles after changing TH to give the controller time to update the data pins. 2 NOP instructions between DATA write and DATA read seems to be enough, based on what games do.
You’re also supposed to remove the Z80 audio CPU from the bus before touching the controller I/O ports, due to a hardware bug that can screw up Z80 memory access timing until the next reset if the Z80 tries to access 68000 memory (e.g. cartridge ROM) immediately after the 68000 accesses a controller I/O port. Games are good about doing this from what I’ve seen; Sega probably started checking for it once they discovered the problem.
In code (68000 assembly), most games have a short input-reading routine that looks almost exactly like this, maybe doing TH=1 and TH=0 in the reverse order:
lea (0xA10003), a0 ; P1 DATA, set to 0xA10005 instead for P2
; Read TH=0 buttons (A, Start) shifted left 2
move.b #0x00, (a0)
nop
nop
move.b (a0), d0
lsl.b #2, d0
andi.b #0xC0, d0
; Read TH=1 buttons (D-Pad, B, C)
move.b #0x40, (a0)
nop
nop
move.b (a0), d1
andi.b #0x3F, d1
; Combine into a single 8-bit value, and invert so that 1 = pressed
or.b d1, d0
not.b d0
; Then write D0 to RAM somewhere
Simple enough. Now let’s look at two games that do this wrong!
First up, Micro Machines.

I swear, this game does something odd on every platform it released on…
Here’s a disassembly of how Micro Machines reads inputs (yes there are 13 NOPs in each half):
lea (0xA10009), a0 ; P1 CTRL
lea (0xA10003), a1 ; P1 DATA
; inlining a subroutine call here
move.b #0x40, (a0) ; TH as output, other pins as input
move.b #0x00, (a1) ; TH=0
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
move.b (a1), d0 ; read DATA
rol.w #2, d0
andi.b #0xC0, d0
move.b #0x00, (a0) ; all pins as input???
move.b #0x40, (a1) ; TH=1...but TH is set to input
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
move.b (a1), d1 ; read DATA
andi.b #0x3F, d1
or.b d1, d0
not.b d0
The first half is fine (other than the excessive amount of NOPs), but then it sets the TH pin to input? Huh? Doesn’t that make the following TH=1 write a no-op?
Well, it just so happens that when a pin is set to input but the controller isn’t supplying any voltage to that pin (which the official gamepads do not for TH), it gets pulled high over some short amount of time. The TH=1 DATA write doesn’t do anything, but TH gets pulled high somewhere in the middle of the 13 NOPs, so the DATA read gets the TH=1 buttons anyway. Convenient.
Some unofficial gamepads do supply voltage to TH when it’s set to input, and these are not compatible with Micro Machines because both reads will always get the TH=0 buttons. I know for sure 8BitDo’s Genesis wireless receiver has this issue (along with a few other inaccuracies that mostly don’t affect games), some others probably do too.
I don’t think the exact timing for this is well-known, and it’s possibly not consistent between hardware revisions, so you might decide to ignore the timing aspect and immediately set TH to 1 if a game ever sets TH to input.
Yes, there is another game that breaks if you emulate the controller ports that way.

This is Decap Attack, starring a punnily named protagonist. Here’s what it does to read inputs:
lea (0xA10003), a1 ; P1 DATA
lea (0xA10009), a2 ; P1 CTRL
; a few irrelevant instructions here
move.b #0x00, (a1) ; TH=0
move.b #0x00, (a2) ; oh no, all pins as input
nop
nop
move.b (a1), d0 ; read DATA
lsl.b #2, d0
andi.b #0xC0, d0
move.b #0x40, (a1) ; TH=1...but TH is set to input
move.b #0x40, (a2) ; TH back to output, but after the TH=1 write???
nop
nop
move.b (a1), d1 ; read DATA
andi.b #0x3F, d1
or.b d1, d0
not.b d0
This is almost the standard input-reading code that nearly everything uses, except for those odd CTRL writes. Based on the ANDI masks, the game clearly expects the first DATA read to return the TH=0 buttons and the second to return the TH=1 buttons, but how?
For the first read, it reads DATA much sooner after the CTRL write than Micro Machines does, so presumably the console hasn’t completely pulled TH high yet, or at least the controller hasn’t updated the data pins yet. If you instantly pull TH high when it’s set to input, this read will get TH=1 buttons instead of TH=0 and the game won’t work.
I can think of two possibilities for what happens with the second read:
- The TH=1 write does nothing, but TH has been pulled high by the time the game switches TH back to output, so the second read gets the TH=1 buttons anyway
- The console remembers the last value that was written to the DATA port, and upon CTRL writes it immediately updates the voltage levels of all pins set to output
Given that 8BitDo’s wireless receiver doesn’t seem to have any issues with this game, I think it’s #2. I would prefer to run a test with an official gamepad to confirm what exactly happens, but my only semi-functioning official gamepad seems to have some wiring issues that made it difficult to test anything (yes I need to fix that).
With these two games together, it’s at least possible to infer a pretty narrow range for how long it takes TH to get pulled high. First, Decap Attack’s instructions between the CTRL write and the first DATA read:
nop ; 4 cycles
nop ; 4 cycles
move.b (a1), d0 ; 8 cycles
It expects TH=0 buttons here, so TH can’t get pulled high earlier than 12-16 CPU cycles after the CTRL write.
Micro Machines:
move.b #0x40, (a1) ; 12 cycles
nop ; 4 cycles x13
move.b (a1), d0 ; 8 cycles
Micro Machines expects TH=1 buttons here, so it depends on TH getting pulled high no later than 68-72 CPU cycles after the write. Again, I’d prefer to test on actual hardware to narrow this range further but am not currently able to.
Trouble Shooter / Battle Mania is another game I’ve seen with weird input-reading code, but from what I can tell it’s exactly the same thing that Decap Attack is doing.
Reset
The Mega Drive port of Fantastic Dizzy is supposed to mute its audio when you pause the game, but:
I initially thought I must have missed some edge case in emulating the YM2612 sound chip, but I couldn’t figure out what. When you pause the game, it writes 0x00 to every single YM2612 register except for the panning output registers, where it leaves channels 1/3/4/6 with L output enabled and channels 2/5 with R output enabled.
This is not supposed to mute audio! Writing 0x00 to every register keys off every YM2612 operator, but it also sets every operator’s total level (base attenuation) and ADSR release rate to 0, so it’s effectively saying that any currently-playing operators should jump to max volume and then volume should decrease at the lowest possible rate (release rate 0 does gradually increase attenuation but very slowly). This also makes some instruments sound odd since it does this to both modulators and carriers.
(The proper way to mute everything would be to set every carrier’s release rate to the max possible after keying them off. You could also set every carrier’s total level to max, or disable both L and R output for every channel, but either of those is likely to cause a pop.)
Naturally, the actual problem had nothing to do with the YM2612 itself: the Genesis connects the YM2612’s reset line to the Z80’s reset line, so resetting the Z80 also resets the YM2612, and the game relies on this to mute audio because of course it does. Emulating that behavior completely fixed this.
More?
Potentially! I have a long list, but I also don’t think it’s very interesting to spend multiple paragraphs on 10 different variations of “Sonic 3D Blast’s special stages had a glitchy line because the horizontal interrupt triggered too early and that broke the game’s HV polling logic”. I do have a few in particular that I want to write about as an excuse to look more into why the games depend on what they do.