Sprite RAM Bug

From GbdevWiki
Jump to: navigation, search

The copy of Pan Docs hosted on this wiki is considered deprecated.

Pan Docs is now officially hosted on gbdev.io as a living document. Please go to https://gbdev.io/pandocs/ to read Pan Docs or to https://github.com/gbdev/pandocs to contribute.

Click here to go to this section of Pan Docs in the new location: https://gbdev.io/pandocs/#sprite-ram-bug

There is a flaw in the Game Boy hardware that causes trash to be written to OAM RAM if the following commands are used while their 16-bit content is in the range of $FE00 to $FEFF while the PPU is in mode 2:

 inc rr        dec rr          ;rr = bc,de, or hl
 ldi a,(hl)    ldd a,(hl)
 ldi (hl),a    ldd (hl),a

Sprites 1 & 2 ($FE00 & $FE04) are not affected by this bug.

Game Boy Color and Advance are not affected by this bug.

Accurate Description

The Sprite RAM Bug (or OAM Bug) actually consists of two different bugs:

  • Attempting to read or write from OAM (Including the $FFA0-$FEFF region) while the PPU is in mode 2 (OAM mode) 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 a memory write to OAM, causing a corruption.

Affected Operations

The following operations are affected by this bug:

  • Any memory access instruction, if it accesses OAM
  • inc rr, dec rr - if rr is a 16-bit register pointing to OAM, it will trigger a write and corrupt OAM
  • ldi [hl], a, ldd [hl], a, ldi a, [hl], ldd a, [hl]- these will trigger a corruption twice if hl points to OAM; once for the usual memory access, and once for the extra write trigger by the inc/dec
  • pop rr, the ret family - For some reason, pop will 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 ret instructions.
  • push rr, the call family, rst xx and interrupt handling - Pushing to the stack will trigger the bug 4 times; two usual writes and two glitched write caused by the decrease. However, since one glitched write occur in the same cycle as a actual write, this will effectively behave like 3 writes.
  • Executing code from OAM - If PC is inside OAM (executing FF, i.e.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.

Corruption Patterns

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.

Write Corruption

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 sprites):

  • The first word in the row is replaced with this bitwise expression: ((a ^ c) & (b ^ c)) ^ c, where a is the original value of that word, b is the first word in the preceding row, and c is the third word in the preceding row.
  • The last three words are copied from the last three words in the preceding row.

Read Corruption

A read corruption works similarly to a write corruption, except the bitwise expression is b | (a & c).

Write During Increase/Decrease

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.

Read During Increase/Decrease

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 a is the first word two rows before the currently accessed row, b is the first word in the preceding row (the word being corrupted), c is the first word in the currently accessed row, and d is 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
  • Regardless of wether the previous corruption occurred or not, a normal read corruption is then applied.