The Nintendo Game Boy (DMG, 1989) and Game Boy Color (CGB, 1998) are built around the Sharp SM83 CPU — a unique hybrid borrowing the Z80 register set and 8080 core philosophy but implementing neither's full instruction set — driving a tile-based PPU, 4-channel APU, and memory-mapped I/O across a 16-bit address bus.
This reference documents every subsystem at implementation level for a static recompilation engine, covering the full instruction set with cycle-accurate timing, the pixel FIFO renderer, all memory bank controllers, the complete I/O register map, and the hardware errata (OAM bug, HALT bug, STAT blocking, wave RAM corruption) that define correct emulation.
CHAPTER 01CPU — Sharp LR35902 (SM83)
Architecture: neither Z80 nor 8080
The SM83 is an independent Sharp design whose ISA borrows selectively from both the Intel 8080 and Zilog Z80. Die photos confirm a completely different internal implementation from either ancestor. The CPU runs at 4.194304 MHz (2²² Hz), with all timing measured in machine cycles (M-cycles) of 4 clock ticks each.
From the 8080: the core register set (A, F, B, C, D, E, H, L, SP, PC), register pairing (AF, BC, DE, HL), basic 8-bit load/ALU instructions, RST vectors, conditional CALL/RET/JP on Z/NZ/C/NC, and HALT/DI/EI/DAA/CPL/SCF/CCF.
From the Z80: CB-prefix bit operations (BIT, SET, RES, rotates/shifts), relative jumps (JR/JR cc), and Z80-style mnemonics (LD instead of MOV).
Unique to SM83: SWAP (exchange nibbles), LDH (high-page I/O at $FF00+n), LD (HL+)/LD (HL-) with auto-increment/decrement, ADD SP,e8 and LD HL,SP+e8 (signed stack pointer arithmetic), LD (u16),SP (store SP to memory), and STOP.
Removed entirely: all IX/IY index registers and DD/FD prefix instructions, all ED-prefix instructions (no block moves LDIR/LDDR/CPIR, no 16-bit subtract with carry, no IM instructions, no RLD/RRD, no NEG, no RETN), shadow registers (no AF'/BC'/DE'/HL', no EXX), EX DE,HL, IN/OUT port instructions, interrupt modes IM 0/1/2, I and R registers, and all conditionals on Sign and Parity/Overflow flags.
11 undefined opcodes ($D3, $DB, $DD, $E3, $E4, $EB, $EC, $ED, $F4, $FC, $FD) permanently lock the CPU — the decode ROM yields all zeroes, preventing even interrupt dispatch. Only a hardware reset recovers. For a recompiler, these are unreachable traps.
Register file
| Register | Size | Purpose |
|---|---|---|
| A | 8-bit | Accumulator |
| F | 8-bit | Flags (bits 7–4 used; bits 3–0 hardwired to 0) |
| B, C | 8-bit each | General purpose; C also used for $FF00+C addressing |
| D, E | 8-bit each | General purpose |
| H, L | 8-bit each | General purpose; HL is primary memory pointer |
| SP | 16-bit | Stack Pointer |
| PC | 16-bit | Program Counter |
Register pairsAF, BC, DE, HL (16-bit). No shadow registers exist. POP AF masks F bits 3–0 to zero. An internal IME (Interrupt Master Enable) flag, not memory-mapped, is controlled solely by DI, EI, and RETI.
F register layout:
| Bit | Flag | Name |
|---|---|---|
| 7 | Z | Zero |
| 6 | N | Subtract |
| 5 | H | Half-Carry |
| 4 | C | Carry |
| 3–0 | — | Always 0 |
Complete instruction set with cycle counts
All cycle counts below are in T-cycles (divide by 4 for M-cycles). Conditional instructions show taken/not-taken. Flag notation: Z=Zero, N=Subtract, H=Half-carry, C=Carry; 0/1 means always cleared/set, - means unchanged.
Miscellaneous/Control:
| Opcode | Mnemonic | Bytes | T-cycles | Flags ZNHC |
|---|---|---|---|---|
| $00 | NOP | 1 | 4 | ---- |
| $10 | STOP | 1 | 4 | ---- |
| $27 | DAA | 1 | 4 | Z-0C |
| $2F | CPL | 1 | 4 | -11- |
| $37 | SCF | 1 | 4 | -001 |
| $3F | CCF | 1 | 4 | -00C |
| $76 | HALT | 1 | 4 | ---- |
| $F3 | DI | 1 | 4 | ---- |
| $FB | EI | 1 | 4 | ---- |
8-bit Loads: LD r,r' (register-to-register): 1 byte, 4T. LD r,(HL) or LD (HL),r: 1 byte, 8T. LD r,u8: 2 bytes, 8T. LD (HL),u8: 2 bytes, 12T. Indirect A loads through BC/DE: 1 byte, 8T. LD A,(u16) and LD (u16),A: 3 bytes, 16T. Auto-increment/decrement LD (HL+),A / LD A,(HL+) / LD (HL-),A / LD A,(HL-): 1 byte, 8T. High-page LDH ($FF00+u8),A and LDH A,($FF00+u8): 2 bytes, 12T. LDH ($FF00+C),A and LDH A,($FF00+C): 1 byte, 8T. None of these affect flags.
16-bit Loads: LD rr,u16: 3 bytes, 12T. LD (u16),SP: 3 bytes, 20T. LD SP,HL: 1 byte, 8T. LD HL,SP+i8: 2 bytes, 12T, flags 00HC (computed on low byte). PUSH rr: 1 byte, 16T. POP rr: 1 byte, 12T. POP AF sets all four flags from the stack value (lower 4 bits masked to 0).
8-bit ALU (operand is register: 4T; (HL): 8T; immediate: 8T)
| Operation | Flags ZNHC | Notes |
|---|---|---|
| ADD A,x | Z0HC | |
| ADC A,x | Z0HC | Add with carry |
| SUB x | Z1HC | |
| SBC A,x | Z1HC | Subtract with carry |
| AND x | Z010 | H always set |
| XOR x | Z000 | |
| OR x | Z000 | |
| CP x | Z1HC | Compare (SUB without storing) |
| INC r | Z0H- | C unchanged; (HL) variant: 12T |
| DEC r | Z1H- | C unchanged; (HL) variant: 12T |
Half-carry rules: For ADD/ADC 8-bit, H is set when carry from bit 3: ((A & 0xF) + (val & 0xF) + carry_in) > 0xF. For SUB/SBC/CP, H is set when borrow from bit 4: (A & 0xF) < (val & 0xF) + carry_in. INC: H set when (r & 0xF) == 0xF. DEC: H set when (r & 0xF) == 0x0. ADD HL,rr: H = carry from bit 11, C = carry from bit 15. ADD SP,e8 and LD HL,SP+e8: H/C computed on low byte only — H = ((SP & 0xF) + (e8 & 0xF)) > 0xF, C = ((SP & 0xFF) + (e8 & 0xFF)) > 0xFF, Z and N always cleared.
DAA implementation:
correction = 0; set_C = false
if (H || (!N && (A & 0x0F) > 9)): correction |= 0x06
if (C || (!N && A > 0x99)): correction |= 0x60; set_C = true
A = N ? A - correction : A + correction
Z = (A == 0); H = 0; C = set_C; N unchanged16-bit ALU: INC rr / DEC rr: 1 byte, 8T, no flags affected. ADD HL,rr: 1 byte, 8T, flags -0HC (carry from bits 11 and 15). ADD SP,e8: 2 bytes, 16T, flags 00HC.
Accumulator Rotates (non-CB prefix): RLCA ($07), RRCA ($0F), RLA ($17), RRA ($1F) — all 1 byte, 4T, flags 000C. Z is always cleared (unlike CB-prefixed equivalents which set Z based on result).
Jumps, Calls, Returns:
| Opcode | Mnemonic | Bytes | T-cycles | Notes |
|---|---|---|---|---|
| $C3 | JP u16 | 3 | 16 | Unconditional |
| $E9 | JP HL | 1 | 4 | PC = HL (NOT dereferenced) |
| $C2/$CA/$D2/$DA | JP cc,u16 | 3 | 16/12 | Conditional |
| $18 | JR i8 | 2 | 12 | Relative |
| $20/$28/$30/$38 | JR cc,i8 | 2 | 12/8 | Conditional relative |
| $CD | CALL u16 | 3 | 24 | Push PC, jump |
| $C4/$CC/$D4/$DC | CALL cc,u16 | 3 | 24/12 | Conditional |
| $C9 | RET | 1 | 16 | Pop PC |
| $C0/$C8/$D0/$D8 | RET cc | 1 | 20/8 | Condition check adds 1 M-cycle |
| $D9 | RETI | 1 | 16 | RET + IME=1 immediately |
| $C7–$FF (every 8) | RST n | 1 | 16 | CALL to $00/$08/$10/$18/$20/$28/$30/$38 |
CB-prefixed opcodes (all 2 bytes; register operand: 8T, (HL): 16T except BIT (HL): 12T)
| Range | Mnemonic | Flags ZNHC |
|---|---|---|
| CB 00–07 | RLC r/(HL) | Z00C |
| CB 08–0F | RRC r/(HL) | Z00C |
| CB 10–17 | RL r/(HL) | Z00C |
| CB 18–1F | RR r/(HL) | Z00C |
| CB 20–27 | SLA r/(HL) | Z00C |
| CB 28–2F | SRA r/(HL) | Z00C (bit 7 preserved) |
| CB 30–37 | SWAP r/(HL) | Z000 (SM83 unique) |
| CB 38–3F | SRL r/(HL) | Z00C (bit 7 = 0) |
| CB 40–7F | BIT b,r/(HL) | Z01- |
| CB 80–BF | RES b,r/(HL) | ---- |
| CB C0–FF | SET b,r/(HL) | ---- |
Bit number is encoded in bits 5–3 of the second byte. Register encoding in bits 2–0: B=0, C=1, D=2, E=3, H=4, L=5, (HL)=6, A=7. The CB prefix is non-interruptible — no interrupt can fire between the prefix and the operand byte.
HALT, STOP, and the HALT bug
HALT ($76) suspends CPU execution until (IE & IF & 0x1F) != 0. The main clock keeps running; all peripherals continue operating. With IME=1, when an interrupt fires the CPU wakes, dispatches the interrupt normally, and RETI returns to the instruction after HALT. With IME=0 and no pending interrupt, the CPU halts, wakes on any pending interrupt, but does not dispatch — execution simply continues after HALT.
The HALT bug triggers when IME=0 AND (IE & IF & 0x1F) != 0 at the time HALT executes. The CPU exits HALT immediately but fails to increment PC. The byte after HALT is read twice — first as the opcode fetch, then again as either the same opcode or the first operand byte. For multi-byte instructions following HALT, this produces corrupted decoding: HALT; LD A,($1234) [bytes 76 FA 34 12] becomes HALT; LD A,($34FA); INC D because $FA is consumed twice, shifting the operand stream. HALT followed by HALT creates an infinite hang.
EI + HALT interaction: EI's IME latch takes effect during HALT's execution cycle. If EI immediately precedes HALT with a pending interrupt, normal interrupt dispatch occurs instead of the HALT bug.
STOP ($10) enters very low power standby, stopping both the system and main clocks. On GBC, STOP is the mechanism for speed switching (write $01 to KEY1, then execute STOP). The CPU wakes on a joypad button press.
Interrupt system
Five interrupt sources, in priority order
| Bit | Source | Vector | Priority |
|---|---|---|---|
| 0 | VBlank | $0040 | Highest |
| 1 | LCD STAT | $0048 | |
| 2 | Timer | $0050 | |
| 3 | Serial | $0058 | |
| 4 | Joypad | $0060 | Lowest |
IE ($FFFF) enables interrupts per-bit. IF ($FF0F) holds pending interrupt flags (R/W; upper 3 bits read as 1). IME is the master switch, controlled only by DI (immediate), EI (delayed by one instruction), and RETI (immediate).
Dispatch condition: between instructions, if IME=1 AND (IE & IF & 0x1F) != 0, the lowest-numbered set bit determines the vector.
Dispatch takes 5 M-cycles (20 T-states): M0: internal delay. M1: SP decremented. M2: push PC high byte. M3: push PC low byte + load vector into PC. M4: fetch first opcode of handler. The corresponding IF bit is cleared and IME set to 0.
Dispatch is not atomic. The vector is determined in M3 from the current IE & IF. If a higher-priority interrupt becomes pending between M0 and M3, the vector changes. If IE & IF becomes 0 during dispatch (e.g., software cleared IF), the CPU jumps to $0000 — a bugged interrupt.
EI followed by DI does not open an interrupt window. EI's effect is latched for the next instruction; DI's special circuitry defers dispatch by one cycle. No interrupt can fire between them.
CHAPTER 02PPU (Pixel Processing Unit)
Tile format and data storage
Each tile is 8×8 pixels, 2 bits per pixel, 16 bytes using interleaved bitplane encoding. For each row, byte 0 is the low bitplane and byte 1 is the high bitplane. Pixel N (0 = leftmost/MSB) has color ID: ((high_byte >> (7-N)) & 1) << 1 | ((low_byte >> (7-N)) & 1).
Tile data occupies VRAM $8000–$97FF in three blocks of 128 tiles each. LCDC bit 4 selects the addressing mode: when set, the "$8000 method" uses unsigned tile indices 0–255 from base $8000; when clear, the "$9000 method" uses signed indices -128 to +127 from base $9000. Block 1 ($8800–$8FFF) is shared by both modes. Sprites always use $8000 unsigned addressing.
On DMG, color IDs 0–3 map to 4 shades (white, light gray, dark gray, black) through palette registers. On GBC, each tile references one of 8 palettes providing 4 colors each from 32,768 RGB555 colors.
Background layer
A 32×32 tile map (1024 bytes, 256×256 pixels) at either $9800–$9BFF or $9C00–$9FFF (selected by LCDC bit 3) defines the background. The map wraps seamlessly. Scrolling is controlled by SCX ($FF43) and SCY ($FF42):
bgX = (screenX + SCX) & 255
bgY = (screenY + SCY) & 255
tileCol = bgX / 8; tileRow = bgY / 8
pixelX = bgX % 8; pixelY = bgY % 8
tileMapAddr = base + tileRow * 32 + tileColOn GBC, VRAM bank 1 at the same tile map addresses stores attribute bytes: bits 0–2 select BG palette (BGP0–BGP7), bit 3 selects tile VRAM bank, bit 5 enables horizontal flip, bit 6 enables vertical flip, and bit 7 sets BG-to-OAM priority (when set, BG colors 1–3 draw over all sprites).
Window layer
The window is a second background layer with no wrap-around, positioned via WY ($FF4A) and WX ($FF4B). Actual screen X = WX − 7 (so WX=7 places the window at the left edge). The window tile map is selected by LCDC bit 6.
The window maintains an internal line counter separate from LY that only increments on scanlines where the window was actually rendered. Disabling the window mid-frame and re-enabling it causes rendering to resume from the saved counter position — this is the mechanism games use for status-bar/split-screen effects. The WY condition (WY == LY) is checked at Mode 2 start and, once latched true for the frame, remains true regardless of subsequent WY changes.
Sprites and OAM
40 sprites occupy OAM ($FE00–$FE9F), 4 bytes each
| Byte | Content | Details | |
|---|---|---|---|
| 0 | Y position | Screen Y = value − 16 | |
| 1 | X position | Screen X = value − 8 | |
| 2 | Tile index | Unsigned; in 8×16 mode, top = index & $FE, bottom = index | $01 |
| 3 | Attributes | Bit 7: BG-over-OBJ priority. Bit 6: Y-flip. Bit 5: X-flip. DMG bit 4: palette (OBP0/OBP1). GBC bit 3: VRAM bank. GBC bits 2–0: OBJ palette (OBP0–OBP7). |
Sprite size (8×8 or 8×16) is a global setting controlled by LCDC bit 2. A maximum of 10 sprites per scanline can be selected — the PPU scans OAM sequentially during Mode 2, selecting the first 10 whose Y range overlaps the current LY. Sprites with off-screen X coordinates still count toward this limit.
Priority on DMG: smaller X coordinate wins; ties broken by earlier OAM position. Priority on GBC: only OAM position matters (earlier = higher priority), regardless of X. The winning sprite's BG-over-OBJ bit is checked only after inter-object priority is resolved — a high-priority sprite with this bit set will mask lower-priority sprites even if those don't have it set.
PPU modes and frame timing
Each frame consists of 154 scanlines × 456 dots = 70,224 dots, producing a ~59.7275 Hz refresh rate. One dot equals one T-cycle (4.19 MHz). Lines 0–143 are active display; lines 144–153 are VBlank.
| Mode | Name | Duration | VRAM | OAM |
|---|---|---|---|---|
| 2 | OAM Scan | 80 dots (fixed) | Accessible | Blocked |
| 3 | Pixel Transfer | ~172–289 dots (variable) | Blocked | Blocked |
| 0 | HBlank | Remainder of 456 dots | Accessible | Accessible |
| 1 | VBlank | 4,560 dots (10 scanlines) | Accessible | Accessible |
Mode 3 variable length has a base of ~172 dots plus penalties: SCX % 8 dots for fine scroll discard at scanline start, +6 dots if the window activates, and +6 to +11 dots per sprite (6 for the sprite tile fetch plus 0–5 dots waiting for the background fetcher to complete its current cycle). Maximum Mode 3 with 10 worst-case sprites reaches ~289 dots.
STAT register ($FF41) and interrupt behavior
Bit 7: Unused (reads 1)
Bit 6: LYC=LY interrupt enable (R/W)
Bit 5: Mode 2 interrupt enable (R/W)
Bit 4: Mode 1 interrupt enable (R/W)
Bit 3: Mode 0 interrupt enable (R/W)
Bit 2: LYC=LY coincidence flag (Read only)
Bits 1–0: PPU mode (Read only)The STAT interrupt fires on the rising edge of a combined signal: stat_line = (bit6 AND LYC==LY) OR (bit5 AND mode==2) OR (bit4 AND mode==1) OR (bit3 AND mode==0). Because it is edge-triggered, if the line stays high across a mode transition (e.g., Mode 0 → Mode 2 with both enabled), no new interrupt fires. This STAT interrupt blocking behavior is critical for accurate emulation.
DMG STAT write bug: On monochrome DMG hardware, writing any value to STAT during modes 0, 1, or 2 (or when LYC=LY) spuriously triggers a STAT interrupt. The register behaves as if $FF is written for one M-cycle before the actual value takes effect. This bug does not exist on GBC.
Pixel FIFO renderer
The PPU uses two separate FIFOs — a background FIFO and a sprite FIFO — each holding up to 16 pixels. A pixel fetcher fills the background FIFO through a 4-step pipeline, each step taking 2 dots:
- Get tile number: read tile index from the tile map
- Get tile data low: read low bitplane byte from tile data
- Get tile data high: read high bitplane byte
- Push: push 8 pixels to BG FIFO (only if FIFO has ≤8 pixels; retry next dot if full)
When the pixel output position reaches a sprite's X coordinate, background rendering halts. The fetcher waits 0–5 dots for any in-progress BG fetch to complete, then performs a 6-dot sprite tile fetch. Sprite pixels are written into the sprite FIFO, replacing only transparent (color 0) entries — this is why earlier-position sprites have priority on DMG (their pixels are already in the FIFO).
Pixel mixing at output: if the sprite pixel is transparent (color 0), output the BG pixel. If the sprite's BG-over-OBJ flag is set and the BG pixel is non-zero, output the BG pixel. Otherwise, output the sprite pixel. On DMG with LCDC bit 0 clear, the BG always outputs white. On GBC with LCDC bit 0 clear, sprites always display on top regardless of priority flags.
At scanline start, Mode 3 begins by clearing both FIFOs and performing two initial BG tile fetches (the first is discarded). The first SCX % 8 pixels are popped without rendering to achieve fine scrolling.
LCDC register ($FF40)
| Bit | Function | Details |
|---|---|---|
| 7 | LCD & PPU enable | Must only disable during VBlank |
| 6 | Window tile map | 0=$9800, 1=$9C00 |
| 5 | Window enable | Toggleable mid-frame |
| 4 | BG & Window tile data | 0=$9000 signed, 1=$8000 unsigned |
| 3 | BG tile map | 0=$9800, 1=$9C00 |
| 2 | Sprite size | 0=8×8, 1=8×16 |
| 1 | Sprite enable | |
| 0 | BG/Window enable (DMG) / priority (GBC) |
LCDC is never locked by the PPU — the CPU can always write to it, even during Mode 3.
DMG palette registers
BGP ($FF47), OBP0 ($FF48), OBP1 ($FF49)each byte encodes 4 2-bit shade entries (bits 7–6 for color 3, bits 5–4 for color 2, bits 3–2 for color 1, bits 1–0 for color 0). Values: 0=white, 1=light gray, 2=dark gray, 3=black. For sprites, color 0 is always transparent. Final shade: palette >> (color_id * 2) & 0x03.
OAM DMA ($FF46)
Writing value $XX starts an immediate transfer of 160 bytes from $XX00–$XX9F to $FE00–$FE9F, taking 160 M-cycles. During transfer, the CPU can only access HRAM ($FF80–$FFFE) on DMG — all other reads return garbage. The standard DMA routine (10 bytes, copied to HRAM) busy-waits with a 40-iteration dec a; jr nz loop.
CHAPTER 03GBC enhancements
Double-speed CPU mode
KEY1 ($FF4D)bit 7 indicates current speed (read-only), bit 0 arms the speed switch (read/write). To switch: set IE=$00, JOYP=$30, write $01 to KEY1, execute STOP. The CPU halts for 2,050 M-cycles during the transition. In double-speed mode, the CPU runs at 8.388608 MHz. The PPU, HDMA, and sound system are unaffected — still running at normal speed. The timer/DIV registers, serial port, and OAM DMA run at 2× speed.
VRAM banking
VBK ($FF4F) bit 0 selects VRAM bank 0 or 1 for CPU access. Bank 0 contains standard tile data and tile maps (identical to DMG). Bank 1 contains additional tile data at $8000–$97FF and BG map attribute bytes at $9800–$9FFF (palette number, VRAM bank, flip flags, BG priority). The PPU always has simultaneous access to both banks regardless of VBK state.
Color palettes
The GBC has 8 BG palettes and 8 OBJ palettes, each containing 4 colors, stored in dedicated palette RAM (64 bytes BG, 64 bytes OBJ). Colors are RGB555 (little-endian): low byte = gggrrrrr, high byte = 0bbbbbgg. This gives 5 bits per channel, 32,768 possible colors.
Access is through index/data register pairs
- BCPS ($FF68): BG palette index (bits 5–0) with auto-increment on write (bit 7)
- BCPD ($FF69): BG palette data read/write at current BCPS address
- OCPS ($FF6A) / OCPD ($FF6B): identical mechanism for OBJ palettes
Palette N, Color C is at byte index (N × 8) + (C × 2). Palette RAM is inaccessible during Mode 3 (reads return $FF, writes ignored). Auto-increment triggers even if the write itself fails during Mode 3.
HDMA (HBlank DMA)
Registers $FF51–$FF55 control high-speed VRAM transfers. HDMA1/HDMA2 set the source address (16-byte aligned, valid ranges: $0000–$7FF0 or $A000–$DFF0). HDMA3/HDMA4 set the destination (always VRAM $8000–$9FF0, 16-byte aligned). HDMA5 controls the transfer:
General-purpose DMA (bit 7 = 0): transfers all data at once, CPU halted during the entire transfer. HBlank DMA (bit 7 = 1): transfers 16 bytes per HBlank on scanlines 0–143, with the CPU running between transfers. No transfers occur during VBlank. Writing $00 to HDMA5 bit 7 cancels an active HBlank DMA. Transfer length = (HDMA5 & 0x7F + 1) × 16 bytes, maximum 2,048 bytes.
WRAM banking
SVBK ($FF70) bits 2–0 select WRAM bank 1–7 for $D000–$DFFF. Writing $00 selects bank 1. Bank 0 at $C000–$CFFF is always fixed. Total GBC WRAM: 32 KB (8 × 4 KB banks).
DMG compatibility mode
When the CGB flag ($0143) has bit 7 clear, the GBC enters DMG compatibility mode. The boot ROM selects auto-colorization palettes through a title checksum algorithm: it sums all 16 bytes of the title area ($0134–$0143), looks up the result in an internal table, and uses the 4th title character ($0137) to disambiguate collisions. Only Nintendo-published games (licensee code $01) receive custom palettes; third-party titles get a default. Players can override palettes via button combos during the boot animation (Up = gray, Down = green, Left = dark blue, Right = brown, A = blue, B = red, with combinations).
CHAPTER 04Complete memory map
| Address Range | Size | Contents |
|---|---|---|
| $0000–$3FFF | 16 KB | ROM Bank 0 (fixed) |
| $4000–$7FFF | 16 KB | ROM Bank 1–N (switchable via MBC) |
| $8000–$9FFF | 8 KB | VRAM (bank-switchable on GBC) |
| $A000–$BFFF | 8 KB | External/Cartridge RAM (switchable) |
| $C000–$CFFF | 4 KB | WRAM Bank 0 (fixed) |
| $D000–$DFFF | 4 KB | WRAM Bank 1–7 (switchable on GBC; always 1 on DMG) |
| $E000–$FDFF | ~8 KB | Echo RAM (mirror of $C000–$DDFF) |
| $FE00–$FE9F | 160 B | OAM (sprite attribute table) |
| $FEA0–$FEFF | 96 B | Unusable (behavior varies by model) |
| $FF00–$FF7F | 128 B | I/O Registers |
| $FF80–$FFFE | 127 B | HRAM (High RAM) |
| $FFFF | 1 B | IE (Interrupt Enable) |
$FEA0–$FEFF behavior: DMG reads return $00. CGB rev 0–D returns revision-specific masked values. CGB rev E/AGB returns high nibble of lower address byte repeated.
Complete I/O register map ($FF00–$FF7F)
| Address | Name | Description |
|---|---|---|
| $FF00 | P1/JOYP | Joypad: bits 5–4 select button group (W), bits 3–0 return button state (R), active-low |
| $FF01 | SB | Serial transfer data |
| $FF02 | SC | Serial control: bit 7 start, bit 1 CGB speed, bit 0 clock select |
| $FF03 | — | Unused ($FF) |
| $FF04 | DIV | Divider register (upper 8 bits of 16-bit system counter), write resets to $00 |
| $FF05 | TIMA | Timer counter |
| $FF06 | TMA | Timer modulo (reload value) |
| $FF07 | TAC | Timer control: bit 2 enable, bits 1–0 clock select |
| $FF08–$FF0E | — | Unused ($FF) |
| $FF0F | IF | Interrupt flags (bits 4–0); upper bits read as 1 |
| $FF10 | NR10 | CH1 sweep: bits 6–4 pace, bit 3 direction, bits 2–0 shift |
| $FF11 | NR11 | CH1 duty/length: bits 7–6 duty, bits 5–0 length |
| $FF12 | NR12 | CH1 volume envelope: bits 7–4 volume, bit 3 direction, bits 2–0 period |
| $FF13 | NR13 | CH1 period low (write-only) |
| $FF14 | NR14 | CH1 control: bit 7 trigger, bit 6 length enable, bits 2–0 period high |
| $FF15 | — | Unused ($FF) |
| $FF16 | NR21 | CH2 duty/length |
| $FF17 | NR22 | CH2 volume envelope |
| $FF18 | NR23 | CH2 period low (write-only) |
| $FF19 | NR24 | CH2 control |
| $FF1A | NR30 | CH3 DAC enable (bit 7) |
| $FF1B | NR31 | CH3 length (8-bit) |
| $FF1C | NR32 | CH3 volume: bits 6–5 (00=mute, 01=100%, 10=50%, 11=25%) |
| $FF1D | NR33 | CH3 period low (write-only) |
| $FF1E | NR34 | CH3 control |
| $FF1F | — | Unused ($FF) |
| $FF20 | NR41 | CH4 length (bits 5–0) |
| $FF21 | NR42 | CH4 volume envelope |
| $FF22 | NR43 | CH4 frequency: bits 7–4 clock shift, bit 3 LFSR width, bits 2–0 divisor |
| $FF23 | NR44 | CH4 control |
| $FF24 | NR50 | Master volume: bit 7 VIN→L, bits 6–4 L vol, bit 3 VIN→R, bits 2–0 R vol |
| $FF25 | NR51 | Sound panning: bits 7–4 L enables (CH4–CH1), bits 3–0 R enables |
| $FF26 | NR52 | Audio master: bit 7 power on/off, bits 3–0 channel active (R/O) |
| $FF27–$FF2F | — | Unused ($FF) |
| $FF30–$FF3F | Wave RAM | 32 × 4-bit samples (16 bytes), upper nibble first |
| $FF40 | LCDC | LCD control (see PPU section) |
| $FF41 | STAT | LCD status (see PPU section) |
| $FF42 | SCY | Background scroll Y |
| $FF43 | SCX | Background scroll X |
| $FF44 | LY | LCD Y coordinate (0–153, read-only) |
| $FF45 | LYC | LY compare value |
| $FF46 | DMA | OAM DMA source (write value × $100) |
| $FF47 | BGP | BG palette (DMG) |
| $FF48 | OBP0 | OBJ palette 0 (DMG) |
| $FF49 | OBP1 | OBJ palette 1 (DMG) |
| $FF4A | WY | Window Y position |
| $FF4B | WX | Window X position + 7 |
| $FF4C | KEY0 | CGB boot ROM internal (locked after boot) |
| $FF4D | KEY1 | CGB speed switch: bit 7 current speed (R), bit 0 prepare (R/W) |
| $FF4E | — | Unused ($FF) |
| $FF4F | VBK | CGB VRAM bank select (bit 0) |
| $FF50 | BANK | Boot ROM disable (write non-zero to unmap, permanent) |
| $FF51–$FF52 | HDMA1/2 | CGB HDMA source address |
| $FF53–$FF54 | HDMA3/4 | CGB HDMA destination address |
| $FF55 | HDMA5 | CGB HDMA length/mode/start |
| $FF56 | RP | CGB infrared: bits 7–6 read enable, bit 1 receiving (R), bit 0 LED (R/W) |
| $FF57–$FF67 | — | Unused ($FF) |
| $FF68 | BCPS | CGB BG palette index (bit 7 auto-increment, bits 5–0 address) |
| $FF69 | BCPD | CGB BG palette data |
| $FF6A | OCPS | CGB OBJ palette index |
| $FF6B | OCPD | CGB OBJ palette data |
| $FF6C | OPRI | CGB OBJ priority mode (bit 0: 0=CGB, 1=DMG priority) |
| $FF6D–$FF6F | — | Unused ($FF) |
| $FF70 | SVBK | CGB WRAM bank select (bits 2–0) |
| $FF71 | — | Unused ($FF) |
| $FF72–$FF73 | — | Undocumented R/W registers |
| $FF74 | — | Undocumented (R/W in CGB mode, $FF in DMG mode) |
| $FF75 | — | Undocumented (bits 4–6 R/W) |
| $FF76 | PCM12 | CGB: CH1/CH2 PCM amplitude (read-only) |
| $FF77 | PCM34 | CGB: CH3/CH4 PCM amplitude (read-only) |
| $FF78–$FF7F | — | Unused ($FF) |
CHAPTER 05Memory Bank Controllers
MBC1 (up to 2 MB ROM, 32 KB RAM)
| Address | Function |
|---|---|
| $0000–$1FFF | RAM Enable: write $xA (lower nibble = $A) to enable |
| $2000–$3FFF | ROM Bank Number: 5-bit, selects bank for $4000–$7FFF. Writing $00 maps to $01 (bank 0 bug — also affects $20/$40/$60 → $21/$41/$61) |
| $4000–$5FFF | RAM Bank / Upper ROM Bits: 2-bit register |
| $6000–$7FFF | Banking Mode: 0=simple (2-bit extends ROM bank, $0000 locked to bank 0, RAM locked to bank 0), 1=advanced (2-bit selects RAM bank 0–3, $0000 shows bank $00/$20/$40/$60) |
MBC1M (multicart variant): A18 not connected to ROM. The 2-bit register selects bits 4–5 of the ROM bank number, enabling up to four 256 KB games.
MBC2 (up to 256 KB ROM, 512×4-bit built-in RAM)
ROM bank register at $2000–$3FFF (4 bits, banks 1–15), but only when address bit 8 is set. When bit 8 is clear, the write controls RAM enable instead. Built-in RAM at $A000–$A1FF has only lower 4 bits valid per byte.
MBC3 (up to 2 MB ROM, 32 KB RAM + RTC)
ROM bank at $2000–$3FFF (7 bits, $01–$7F; banks $20/$40/$60 are accessible, unlike MBC1). RAM bank / RTC select at $4000–$5FFF ($00–$03 for RAM, $08–$0C for RTC registers). Latch clock at $6000–$7FFF (write $00 then $01). RTC registers: seconds ($08), minutes ($09), hours ($0A), day low ($0B), day high + flags ($0C: bit 0 = day MSB for 9-bit counter, bit 6 = halt, bit 7 = day overflow).
MBC5 (up to 8 MB ROM, 128 KB RAM)
ROM bank low at $2000–$2FFF (8 bits), ROM bank high at $3000–$3FFF (1 bit) — 9-bit bank number allowing 512 banks = 8 MB. RAM bank at $4000–$5FFF (4 bits, 0–15 = 128 KB). Bank 0 is accessible (no remap). Rumble variants use $4000–$5FFF bit 3 for motor control (limiting RAM to 8 banks / 64 KB).
MBC7 (accelerometer + EEPROM)
Contains an ADXL202 2-axis accelerometer and 93LC56 256-byte EEPROM, accessed via memory-mapped registers in $A000–$BFFF. Accelerometer values are 16-bit, centered at $81D0 with gravity producing ~$70 variation. EEPROM is bit-banged through CLK/DI/DO/CS lines. Used in Kirby Tilt 'n' Tumble.
ROM header ($0100–$014F)
| Address | Field | Details |
|---|---|---|
| $0100–$0103 | Entry point | Usually NOP; JP $0150 |
| $0104–$0133 | Nintendo logo | 48 bytes, verified by boot ROM (CGB checks only first 24) |
| $0134–$0143 | Title | Uppercase ASCII, NUL-padded |
| $0143 | CGB flag | $80=compatible, $C0=CGB-only |
| $0146 | SGB flag | $03=SGB features |
| $0147 | Cartridge type | MBC identification ($00=ROM only through $FF=HuC1) |
| $0148 | ROM size | 32 KB << value (standard: $00=32KB through $08=8MB) |
| $0149 | RAM size | $00=none, $02=8KB, $03=32KB, $04=128KB, $05=64KB |
| $014D | Header checksum | x = 0; for i=$0134 to $014C: x = x - MEM[i] - 1 — verified by boot ROM |
| $014E–$014F | Global checksum | 16-bit big-endian sum of all ROM bytes (not verified) |
CHAPTER 06Audio — DMG APU
Four channels and signal architecture
The APU outputs 4 channels mixed to stereo. Signal flow per channel: Timer → Waveform Generator → Length Counter → Volume/Envelope → Mixer → Panning (NR51) → Master Volume (NR50) → DAC → Output.
Channel 1 (pulse + sweep)square wave with 4 selectable duty cycles (12.5%, 25%, 50%, 75%) and hardware frequency sweep. Channel 2 (pulse): identical to CH1 without sweep. Channel 3 (wave): plays 32 programmable 4-bit samples from Wave RAM at $FF30–$FF3F with 4 volume levels (mute/100%/50%/25%). Channel 4 (noise): LFSR-based with configurable 15-bit or 7-bit mode.
Frame sequencer (DIV-APU)
A 512 Hz counter derived from falling-edge detection on DIV's bit 4 (bit 5 in double-speed mode) clocks the APU's modulation units:
| Step | Length Counter | Volume Envelope | Sweep |
|---|---|---|---|
| 0 | ✓ | ||
| 1 | |||
| 2 | ✓ | ✓ | |
| 3 | |||
| 4 | ✓ | ||
| 5 | |||
| 6 | ✓ | ✓ | |
| 7 | ✓ |
Effective rates: length counter at 256 Hz, volume envelope at 64 Hz, sweep at 128 Hz.
Frequency and period formulas
Pulse (CH1/CH2): output frequency = 131072 / (2048 − period_value) Hz. Wave (CH3): sample rate = 4194304 / ((2048 − period_value) × 2) Hz; audible frequency = sample_rate / 32. Noise (CH4): period = divisor << clock_shift T-cycles, where divisor from code {8, 16, 32, 48, 64, 80, 96, 112}.
Sweep unit (CH1 only)
Sweep formulanew_period = shadow ± (shadow >> shift). On trigger, the shadow register loads from NR13/NR14, and if shift ≠ 0, an immediate overflow check runs (but doesn't write back). On each sweep tick (128 Hz), if the result ≤ 2047 and shift ≠ 0, the new period is written to both the shadow register and NR13/NR14, then a second overflow check runs. If the result ever exceeds 2047, CH1 is immediately disabled. Negate mode quirk: clearing the negate bit after any subtraction since the last trigger immediately disables CH1.
Volume envelope
Clocked at 64 Hz. Period of 0 is treated as 8 internally. When the timer fires: new_vol = volume ± 1. If 0 ≤ new_vol ≤ 15, volume updates; otherwise the envelope stops permanently until retrigger.
Noise channel LFSR
On each timer tickXOR bits 0 and 1, shift register right by 1, place XOR result into bit 14. If width mode = 1 (NR43 bit 3), also place the result into bit 6, creating a 7-bit loop. Output is bit 0 inverted. On trigger, all LFSR bits are set to 1.
Channel trigger behavior (NRx4 bit 7 = 1)
Triggering a channelenables it (if DAC on), reloads the length counter to max if zero, reloads the period timer, reloads envelope volume and timer, reloads sweep state for CH1, resets wave position for CH3, sets all LFSR bits for CH4. If the DAC is off (NRx2 upper 5 bits all zero for CH1/2/4, or NR30 bit 7 = 0 for CH3), the channel immediately disables again.
Audio register read masks
Many audio register bits return 1 when read (write-only fields). Key OR masks: NRx1 reads $3F (length bits masked), NRx3 reads $FF (frequency low completely unreadable), NRx4 reads $BF (trigger bit always reads 1). $FF27–$FF2F all read $FF.
CHAPTER 07Timer system
Registers and the 16-bit internal counter
DIV ($FF04) is the upper 8 bits of a 16-bit system counter that increments every T-cycle (4.194304 MHz). Writing any value to DIV resets the entire 16-bit counter to zero. TIMA ($FF05) is incremented by falling-edge detection on a selected bit of this counter ANDed with the timer enable bit. TMA ($FF06) is the reload value. TAC ($FF07) bit 2 enables the timer; bits 1–0 select the counter bit:
| TAC bits 1–0 | Frequency | Counter bit | CPU cycles per tick |
|---|---|---|---|
| 00 | 4,096 Hz | Bit 9 | 1024 |
| 01 | 262,144 Hz | Bit 3 | 16 |
| 10 | 65,536 Hz | Bit 5 | 64 |
| 11 | 16,384 Hz | Bit 7 | 256 |
Timer overflow with 1 M-cycle delay
When TIMA overflows ($FF → $00), there is a 1 M-cycle delay. During cycle A (overflow), TIMA reads as $00 but TMA is not yet loaded and IF is not yet set. On cycle B, TMA is copied to TIMA and the timer interrupt flag (IF bit 2) is set. Writing to TIMA during cycle A cancels the overflow entirely. Writing to TIMA during cycle B is ignored (overwritten by TMA). Writing to TMA during cycle B causes the new TMA value to also be loaded into TIMA.
DIV-related timing obscurities
Writing to DIV resets the 16-bit counter, which can cause a falling edge on the selected timer bit — spuriously incrementing TIMA. Changing the clock select in TAC can also produce spurious increments if the old selected bit was 1 and the new selected bit is 0. Disabling the timer (TAC bit 2: 1→0) on DMG when the selected bit is 1 likewise causes a spurious increment.
CHAPTER 08Joypad ($FF00)
P1/JOYP uses a 2×4 button matrix with active-low logic
Bit 5 (W): Select action buttons (0=select)
Bit 4 (W): Select direction buttons (0=select)
Bit 3 (R): Down or Start (0=pressed)
Bit 2 (R): Up or Select (0=pressed)
Bit 1 (R): Left or B (0=pressed)
Bit 0 (R): Right or A (0=pressed)To read inputwrite $20 to select directions (read D-pad), then $10 to select actions (read A/B/Select/Start). Both groups selected simultaneously ($00) produces ambiguous OR'd results. The joypad interrupt (IF bit 4) fires on any high-to-low transition of bits 0–3.
CHAPTER 09Boot ROM and initial register state
DMG boot ROM (256 bytes)
The DMG boot ROM initializes the stack pointer to $FFFE, zeroes VRAM, sets up audio registers, loads and displays the Nintendo logo with a scroll-down animation, plays the "ba-ding!" sound, verifies the logo and header checksum (locking up on failure), then writes $01 to $FF50 to unmap itself. Execution begins at $0100.
DMG register state after boot:
| Register | Value | Register | Value |
|---|---|---|---|
| A | $01 | F | $B0 (Z=1,N=0,H=1,C=1) |
| B | $00 | C | $13 |
| D | $00 | E | $D8 |
| H | $01 | L | $4D |
| SP | $FFFE | PC | $0100 |
Key I/O state after DMG boot: LCDC=$91, BGP=$FC, NR50=$77, NR51=$F3, NR52=$F1, OBP0=$FF, OBP1=$FF. MGB (Game Boy Pocket) is identical except A=$FF.
GBC boot ROM (2048 bytes)
The GBC boot ROM performs additional stepscompatibility detection via $0143, auto-colorization palette selection for DMG games (title checksum algorithm), and more elaborate logo animation. Only the first 24 bytes of the Nintendo logo are verified (exploitable).
GBC register state after boot (CGB game):
| Register | Value | Register | Value |
|---|---|---|---|
| A | $11 | F | $80 (Z=1,N=0,H=0,C=0) |
| B | $00 | C | $00 |
| D | $FF | E | $56 |
| H | $00 | L | $0D |
| SP | $FFFE | PC | $0100 |
GBC register state (DMG compatibility mode): A=$11, F=$80, B=title checksum (varies by game), C=$00, D=$00, E=$08, H=$00, L=$7C. On GBA running in GBC mode, B is incremented by 1 (allowing GBA detection via bit 0 of B).
For static recompilation, the boot ROM can be skipped entirely by initializing registers and I/O to the documented post-boot state and starting execution at $0100.
CHAPTER 10Hardware errata and recompilation edge cases
OAM bug (DMG only)
Accessing $FE00–$FEFF while the PPU is in Mode 2 corrupts OAM. The bug triggers not only on explicit OAM reads/writes but also when the CPU's IDU (Increment/Decrement Unit) outputs an address in the OAM range during 16-bit inc/dec operations — even without a bus read/write assertion. Affected instructions include inc rr/dec rr when the register pair points into OAM, ldi/ldd variants (double trigger), pop/ret (triple trigger), and push/call/rst/interrupt dispatch (quadruple trigger).
The corruption pattern overwrites the first word of the currently-scanned OAM row using a formula involving XOR/AND of adjacent row data, then copies words from the preceding row. Not present on GBC/GBA.
HALT bug
When HALT executes with IME=0 and a pending interrupt (IE & IF != 0), the CPU exits immediately but PC fails to increment, causing the next byte to be read twice. For multi-byte instructions following HALT, this shifts the operand stream, producing corrupted decoding. The bug is well-documented and some games intentionally exploit it.
Window internal line counter
The window's line counter resets during VBlank, only increments on scanlines where the window was rendered, and persists through enable/disable cycles. Disabling the window mid-frame and re-enabling it later causes rendering to continue seamlessly from the saved counter — enabling split-screen status bar effects. Changing WY after its match with LY has been latched does not un-trigger the window for the current frame.
STAT interrupt blocking
Because the STAT interrupt fires on the rising edge of the combined condition line, transitions where the line stays high (e.g., Mode 0 → Mode 2 with both interrupt sources enabled) produce no new interrupt. This is the source of the well-known "STAT blocking" behavior that many games rely on.
Wave RAM corruption (DMG only)
Retriggering CH3 while it is actively reading a sample corrupts the first 4 bytes of Wave RAM. If reading bytes 0–3, only byte 0 is overwritten. If reading bytes 4–15, the first 4 bytes are overwritten with the aligned 4-byte group at the current read position. The workaround is to write 0 then $80 to NR30 before retriggering. Reading Wave RAM while CH3 is active on DMG only returns valid data on the exact cycle of CH3's own read — at any other time, reads return $FF.
Memory access restrictions
VRAM reads return $FF and writes are ignored during Mode 3. OAM reads return $FF and writes are ignored during Modes 2 and 3. GBC palette RAM is inaccessible during Mode 3. During OAM DMA, the CPU on DMG can only access HRAM — ROM/RAM reads return the byte currently being transferred. These restrictions must be enforced per-M-cycle for full accuracy.
Practical static recompilation considerations
Every emitted code block must track its cycle cost. Before any I/O register access, the recompiler must synchronize PPU/timer/serial state to the current cycle. Reads and writes to $8000–$9FFF and $FE00–$FE9F require PPU mode checks unless provably safe (e.g., code executes only in VBlank handlers). All accesses to $FF00–$FFFF must go through I/O dispatch and cannot be optimized away.
Static analysis can prove many accesses safe — for instance, code running in a VBlank interrupt handler can access VRAM freely. Bank switching complicates full ROM analysis, and computed jumps (JP HL) require dynamic dispatch tables. Self-modifying code via WRAM is rare but possible. A hybrid approach — pre-compiling known ROM code statically while emitting runtime checks for timing-sensitive I/O and falling back to interpretation for edge cases — offers the best balance of performance and accuracy.
Key game techniques that demand precise timing include raster effects (changing SCX/SCY in STAT handlers for parallax), mid-frame palette writes during Mode 3 on DMG, sprite multiplexing via mid-frame OAM DMA, HBlank VRAM updates, and window splitting for status bars. The interrupt timing window (especially EI's one-instruction delay and STAT edge detection) must be modeled exactly.
CHAPTER 11Conclusion
Public sources
Production ledger
GB-F026HALT wake needs explicit pending-source state
Generated SM83 projects now keep an explicit gb_halted state around HALT instead of falling through after one frame tick. Each logical timing boundary recomputes IE & IF & $1F; an enabled pending source clears the halted state even with IME clear, while the opt-in vector service may then enter a known handler.
GB-F025Interrupt entry needs an explicit opt-in before promotion
Generated projects now include an opt-in --gb-interrupts vector-service contract. For a known handler it pushes the captured return PC, clears the selected IF bit and IME/EI delay, guards nested entry, and counts dispatches and misses.
GB-F024Return-PC provenance must precede interrupt stack mutation
Timing boundaries now derive the architectural next PC from the translated branch instruction length and expose it beside the last source PC. The value is not yet pushed or used to dispatch; indirect control flow, IF/IME mutation, handler execution, and RETI return integration remain open.
GB-F023Interrupt priority needs provenance before handler dispatch
The runtime now computes the lowest set bit of IE & IF & $1F, maps it to the fixed SM83 vector $0040 + 8*bit, and records the pending mask and last translated source PC. The three authorized 3,200-frame runs expose candidate $0040 observations.
GB-F022MBC3 RTC selection and latch state must not alias external RAM
MBC3 types $0F-$13 now use a distinct 7-bit ROM-bank path and treat $08-$0C in the RAM/RTC select register as RTC registers rather than RAM banks. The bus models RAM/RTC enable, the 0 -> 1 latch edge, latched seconds/minutes/hours/day values, deterministic T-cycle advancement, halt, and day carry.
GB-F021RETI must re-enable IME after the full SM83 stack pop
The Game Boy RETI lift now follows the same full 16-bit stack-pop path as RET and immediately restores IME while clearing any delayed-EI state. Focused architecture coverage and the three-title 3,200-frame native regression pass.
GB-F020IME and EI delay need observable state before vector dispatch
The generated SM83 runtime now keeps an explicit interrupt-master-enable latch and one-instruction EI delay. DI clears both immediately, and the instruction boundary records pending sources from IE & IF & $1F; internal evidence expose the resulting IME, delay, and pending-source states for every sampled frame.
GB-F019MBC1 banking mode must not collapse into MBC5 registers
MBC1 cartridge types $01-$03 now have separate lower/upper ROM-bank fields, ROM/RAM banking mode, forbidden-bank remapping, RAM enable, and mode-dependent fixed-window/RAM-bank behavior. MBC5 remains unchanged.
GB-F018HDMA needs register-visible state before dot arbitration
CGB HDMA1–HDMA5 ($FF51-$FF55) now model masked source/destination values, immediate 16-byte general-DMA blocks, queued HBlank blocks, and status length. Source bytes traverse the CPU bus and destinations use the selected VRAM bank; internal evidence records transfer count and active state.
GB-F017Serial link state must not be silently treated as generic RAM
SB/SC ($FF01/$FF02) now have explicit bus handlers. Internal-clock transfers complete after 4,096 T-cycles with open-link $FF input and serial IF signaling; external-clock transfers remain pending.
GB-F016KEY1 and STOP need explicit CGB state before double-speed work
The runtime now models CGB KEY1 ($FF4D) current/prepare speed bits and lifts SM83 STOP into a tracked Game Boy state; a prepared STOP toggles the current speed bit and clears preparation. DMG mode leaves the register inactive, and internal evidence expose KEY1/stopped state.
GB-F015Coarse frame timing cannot safely unlock dot access rules
A bounded native experiment attempted Mode 2/3/0/1 transitions, dot-based LY, and CPU VRAM/OAM blocking. It stalled before the first fresh Rhythm Land frame; deferring the newly asserted Mode-2 STAT edge did not cure the failure.
GB-F014CGB palettes are a separate color pipeline
CGB background and object palette RAM is now modeled as 64 bytes per class. BCPS/BCPD and OCPS/OCPD provide indexed data access with auto-increment, and tile/OAM palette IDs select RGB555 colors in the renderer.
GB-F013OAM and HRAM must share the real Game Boy bus
The generated runtime now keeps OAM and HRAM outside the generic flat backing store. The 13-test architecture suite and three-title 3,200-frame matrix remain green.
GB-F012CGB bank selectors must not be modeled as flat memory
The native Game Boy state now owns dual VRAM and banked WRAM storage. CGB VBK/SVBK writes route CPU-visible VRAM/WRAM accesses, zero SVBK selects bank 1, and DMG mode reads the selectors as inactive.
GB-F011MBC5 routing must precede visual and mechanics proof
The three authorized headers are MBC5 cartridges: Rhythm Land uses $19, while Little Green Blob and Dark Force use $1B. The runtime now detects the header, implements fixed and switchable ROM windows, MBC5 ROM-bank registers, RAM enable, RAM-bank selection, and bounded external RAM storage.
GB-F010Indirect JP (HL) requires target provenance before dispatch
Routing SM83 JP (HL) directly through a full-address native dispatcher was tested as a bounded experiment. The CGB corpus produced an unresolved target $74C9, and both CGB native proofs failed liveness.
GB-F009Timer and STAT timing must be observable before visual rebaseline
The first timing iteration keeps the existing logical scanline boundary but adds a 456-T-cycle accumulator for the SM83 divider. TAC-selected timer edges, the four-T-cycle TIMA reload delay, DIV reset behavior, and STAT rising-edge requests are now emitted in the Game Boy I/O runtime.
GB-F008CB bit operations must preserve byte masks and memory RMW
The first CB-family lift treated the bit index itself as the TEST operand and treated (HL) as an ALU lvalue. That was wrong for SM83: BIT n,r tests 1 << n, while SET n,(HL) and RES n,(HL) must load one byte, update it, and store it back.
GB-F007Indirect vector recovery is a bounded liveness fallback
Some homebrew vector helpers use JP (HL) targets that recursive descent does not yet recover. The current Game Boy-only unresolved-target path performs the callee's return-pop boundary so saved registers remain balanced and the native proof does not fail for the wrong reason.
GB-F006Copied ROM diagnostics must not shadow WRAM reads
The native project copies an internal cartridge into a flat diagnostic buffer. A generic ROM-first reader therefore returned cartridge bytes for work-RAM/stack addresses in C000-DDFF, corrupting translated returns.
GB-F005Startup opcode coverage must be proven at the decoder boundary
The dependency-free SM83 decoder was emitting RST, pair arithmetic, PUSH/POP, INC (HL), CB, and HL post-increment/decrement forms as raw bytes. This caused the generated program to reach a frame hook while losing the real startup control path.
GB-F004SM83 pair width and full-stack semantics are native blockers
The z80 variant's default ALU width is 8 bits, but AF/BC/DE/HL/SP address and stack operations are 16-bit. The first native pass truncated pair state and used the Atari 6502 stack-page convention for Game Boy calls.
GB-F003Native proof artifacts are internal
Source hashes, generated C projects, native executables, SDL2.dll, PPM samples, full replay logs, and TAS event files remain outside the public publication manifest. Public or client-facing output may carry only policy-approved metadata, limitations, and explicit trust state until human rights and acceptance decisions exist.
GB-F002JOYP transport now has attributable replay evidence
The Game Boy TAS contract converts the shared .tas.json schema into flat events containing active-low direction and button nibbles. Native replay logs [TAS] and [GB_INPUT] rows; the three-title matrix records multiple direction and button states for all titles.
GB-F001L1 liveness is not visual correctness
The first three authorized homebrew lifts (Rhythm Land, Little Green Blob, and Dark Force) all build with the local MSVC/CMake/SDL2 toolchain and reach 1,800 native frames. A internal evidence records LCDC=$00, fixed LY, fixed visual CRC, and identical frame CRC across the full run for each title.