There is a flaw in the Game Boy hardware that causes rubbish data to be written to object attribute memory (OAM) if the following instructions are used while their 16-bit content (before the operation) is in the range $FE00–$FEFF and the PPU is in mode 2:
inc rr dec rr ; rr = bc, de, or hl ld a, [hli] ld a, [hld] ld [hli], a ld [hld], a
Objects 0 and 1 ($FE00 & $FE04) are not affected by this bug.
Game Boy Color and Advance are not affected by this bug, even when running monochrome software.
The OAM Corruption Bug (or OAM Bug) actually consists of two different bugs:
- Attempting to read or write from OAM (Including the $FEA0-$FEFF region) while the PPU is in mode 2 (OAM scan) will corrupt it.
- Performing an increase or decrease operation on any 16-bit register (BC, DE, HL, SP or PC) while that register is in the OAM range ($FE00–$FEFF) will trigger an access to OAM, causing a corruption. This happens because the CPU’s increment and decrement unit (IDU) for 16-bit numbers is directly tied to the address bus. During IDU operation, the value is output as an address, even if a read or write is not asserted.
The following operations are affected by this bug:
- Any memory access instruction, if it accesses OAM
dec rr- if
rris a 16-bit register pointing to OAM, it will trigger a write and corrupt OAM
ld [hli], a,
ld [hld], a,
ld a, [hli],
ld a, [hld]- these will trigger a corruption twice if
hlpoints to OAM; once for the usual memory access, and once for the extra write triggered by the
pop rr, the
retfamily - For some reason,
popwill trigger the bug only 3 times (instead of the expected 4 times); one read, one glitched write, and another read without a glitched write. This also applies to the
push rr, the
rst xxand interrupt handling - Pushing to the stack will trigger the bug 4 times; two usual writes and two glitched writes caused by the implied
dec sp. However, since one glitched write occurs in the same cycle as a actual write, this will effectively behave like 3 writes.
- Executing code from OAM - If PC is inside OAM (reading $FF,
rst $38) the bug will trigger twice, once for increasing PC inside OAM (triggering a write), and once for reading from OAM. If a multi-byte opcode is executed from $FDFF or $FDFE, and bug will similarly trigger twice for every read from OAM.
The OAM is split into 20 rows of 8 bytes each, and during mode 2 the PPU reads those rows consecutively; one every 1 M-cycle. The operations patterns rely on type of operation (read/write/both) used on OAM during that M-cycle, as well as the row currently accessed by the PPU. The actual read/write address used, or the written value have no effect. Additionally, keep in mind that OAM uses a 16-bit data bus, so all operations are on 16-bit words.
A “write corruption” corrupts the currently access row in the following manner, as long as it’s not the first row (containing the first two objects):
- The first word in the row is replaced with this bitwise expression:
((a ^ c) & (b ^ c)) ^ c, where
ais the original value of that word,
bis the first word in the preceding row, and
cis the third word in the preceding row.
- The last three words are copied from the last three words in the preceding row.
A “read corruption” works similarly to a write corruption, except the
bitwise expression is
b | (a & c).
If a register is increased or decreased in the same M-cycle of a write, this will effectively trigger two writes in a single M-cycle. However, this case behaves just like a single write.
If a register is increased or decreased in the same M-cycle of a write, this will effectively trigger both a read and a write in a single M-cycle, resulting in a more complex corruption pattern:
- This corruption will not happen if the accessed row is one of the
first four, as well as if it’s the last row:
- The first word in the row preceding the currently accessed row
is replaced with the following bitwise expression:
(b & (a | c | d)) | (a & c & d)where
ais the first word two rows before the currently accessed row,
bis the first word in the preceding row (the word being corrupted),
cis the first word in the currently accessed row, and
dis the third word in the preceding row.
- The contents of the preceding row is copied (after the corruption of the first word in it) both to the currently accessed row and to two rows before the currently accessed row
- The first word in the row preceding the currently accessed row is replaced with the following bitwise expression:
- Regardless of whether the previous corruption occurred or not, a normal read corruption is then applied.