Cartswapping is a technique which allows “transporting” ACE to any game.
What is cartswap?
Cartswap is a simple technique in concept, based on the following principle: pulling the cartridge out of the console does not by itself stop execution. (This is unlike, say, the NES and SNES, where pulling the cartridge, when possible, causes the CIC lockout chip to halt execution.)
Usually, games crash spectacularly when pulling the cartridge, since the ROM address space appears to read
0xFF bytes. This corresponds to a
rst 38h instruction, which calls a subroutine at address
$0038. Thing is,
$0038 points to ROM! The CPU reads one more
0xFF byte, so it pushes the return address (
$0039) to the stack, and jumps to
$0038. This repeats over and over, causing a stack overflow which overwrites the entire memory very rapidly.
(Note: testing has proven it’s not guaranteed that reads return
0xFF, but it’s almost guaranteed after a few instructions.)
However, ACE exploits take execution outside of the ROM space, mostly WRAM, which is not affected by pulling the cartridge!
- Somehow make the CPU execute instructions outside of ROM, and stay there
- Pull the cartridge out
- Insert another cartridge
Let’s break this down. First, getting the CPU outside of ROM. This basically amounts to finding an ACE exploit in any game - fortunately, there are plenty in plenty of games, most notably the Pokémon games.
Second, keeping the CPU outside of ROM. This involves the ACE exploit’s payload. There are three techniques used to do so: either spin the CPU in a loop for a relatively large amount of time (a few seconds, basically); wait until the cartridge has been inserted again; or wait for a user interaction. Let’s discuss the latter two.
Knock knock, is any ROM there?
Detecting the cartridge should be fairly simple: all cartridges must contain the Nintendo logo in their header, starting at address
$0144, otherwise the console locks up when starting. (Though the Game Boy Color only requires the first half of the logo to be present, for some reason.)
In theory, this is very good, since there are a lot of bytes and therefore very little chance of a false positive; in practice, this is difficult to achieve, simply because that’s a large amount of data to store. It’s possible to copy this data from the “donor” cartridge’s header, but that still complicates the payload. It’s also theorized that right as the cartridge is inserted, its connections to the console might not be stable - so after detecting the ROM header, you’d have to wait some time, which feels quite redundant, at least imo.
How to wait?
A good question is how to wait on the user, if we’re going to rely on that. Polling the joypad is a fairly expensive process, and especially in the context of Pokémon ACE, loops are complicated. But, the Game Boy has a facility that helps us tremendously:
stop and the joypad interrupt.
A little bit of context, if you’re not familiar with the Game Boy: one of the instructions supported by the CPU is
stop. This instruction makes the Game Boy enter a sort of “sleep mode”: the LCD, sound circuitry, and CPU stop, only to wake up when the CPU receives an interrupt.
The joypad interrupt is an interrupt that, roughly, fires when a button gets pressed. We can use this interrupt to wake up the CPU from
stop! In practice, this isn’t that easy, as it’s required to write to a couple of memory-mapped hardware registers, but it works.
When in doubt, please don’t reboot
Sometimes, when doing cartswap, all seems fine… but the console resets on its own. Why? Well, it appears that the CPU, like all good chips, has a /RESET signal. What’s interesting is, that signal is exposed to the cartridge! It makes sense: when the CPU resets, whatever hardware is on the cartridge (usually a MBC, but why not a coprocessor?) should reset as well.
Fortunately for flashcarts, unfortunately for us, this goes the other way around as well: the cartridge can reset the CPU! And it does happen sometimes. I determined a 33~50% failure rate. Bearable, but ouch. This makes cartswap an unreliable trick, so I consider a single-swap trick feasible, two-step is up to 75% failure so bearable, and above should be avoided.
Why exactly this occurs is not known for sure, but the common assumption is a non-shared ground for a moment as the cartridge is plugged in.
OAM DMA hijacking
This technique greatly enhances ACE possibilities, and is portable to (almost?) all the Game Boy library. It works because of one of the console’s quirks, and basically allows for a mostly portable, “real-time” ACE.
The Game Boy’s sprites (“objects”) are defined in a section of memory known as OAM. Since the PPU (pixel-processing unit, the primitive renderer chip) is accessing it as well, OAM is annoying to access. And it has to be altered on every frame! (Roughly. Not always, but it often happens.) So, the Game Boy has a facility known as a DMA, that allows transferring data to OAM very quickly.
This main upside is that the transfer is fast, which is a premium on a 1 MHz system. The main downside is that, while the DMA is in progress, the CPU isn’t stopped, and cannot access memory either! Except for a tiny (127 bytes) portion of RAM known as HRAM. Thus, all games that use OAM DMA copy a small routine to HRAM on startup, and run this routine roughly every frame.
OAM DMA hijacking works by altering this routine and redirect execution to some of our own code: a hook. That way, we get a hook that runs on every frame, which is quite powerful! And, since all games I know of use OAM DMA, this is basically portable to every game in the library.
The standard OAM DMA routine that games use is the following:
PerformDMA:: ; Somewhere in HRAM ld a, ldh , a ; $FF46 ld a, $28 .wait dec a jr nz, ret
The simplest way to patch this routine is to replace it with
jp DMAHook (
DMAHook being where our hook routine is stored.)
This method is simple, but has a big loss: the game is now unable to update sprites!
We can instead find three free bytes of HRAM, put our
jp DMAHook there, and replace the
ld a, HIGH(wOAMBuffer) with
jr <location of jp DMAHook>. Then, we need to have our hook jump back to the
ldh [rDMA], a with the right value in
While this does fix v1’s problem, it’s quite convoluted, and requires finding three consecutive free bytes in HRAM, which is far from being a given.
We can replace the first two instructions with
call DMAHook, then
ld [$FF00+c], a; we simply need
a to be the correct value and
c to contain
$46 when the hook returns.
This is basically the ideal method, since the game is still able to update OAM, and all we need is to tack a
ld c, $46 and
ld a, HIGH(wOAMBuffer) after our hook function.
We now have a function that gets executed on every frame. (Almost, see details below.) What can we do with this?
For starters, we can make our own Action Replay (fun fact: the original Action Replay worked by hijacking the VBlank interrupt, so we’ll be replicating a very close effect). Simply modify memory addresses to your heart’s content!
A more, much more complex effect is code flow manipulation. See, we have access to the program stack, so we can (conditionally) manipulate code flow. Using this, I’ve been able to prevent opening the START menu in pokéstryder.
Most games do OAM DMA on each frame; not all do. This means that our execution is still at the mercy of the game.