An interactive guide to emulating Game Boy on Game Boy Advance
ChromA runs Game Boy and Game Boy Color games on Game Boy Advance hardware. It maps a Z80-derived CPU onto ARM assembly, converts 2bpp tiles to 4bpp on the fly, and fits the entire CPU core in 32 KB of fast IWRAM.
0000-3FFF ROM Bank 0 (16 KB) 4000-7FFF ROM Bank 1-N (switchable) 8000-9FFF VRAM (8K, 2 banks on GBC) A000-BFFF External SRAM C000-CFFF WRAM Bank 0 (4 KB) D000-DFFF WRAM Bank 1-7 (GBC switchable) FE00-FE9F OAM (sprite attributes) FF00-FF7F I/O Registers FF80-FFFE HRAM (127 bytes, fast) FFFF Interrupt Enable
00000000 BIOS (16 KB) 02000000 EWRAM (256 KB, 16-bit bus) 03000000 IWRAM (32 KB, 32-bit bus) 04000000 I/O Registers 05000000 Palette RAM (1 KB) 06000000 VRAM (96 KB) 07000000 OAM (1 KB) 08000000 Game Pak ROM (up to 32 MB) 0E000000 Game Pak SRAM (64 KB)
The GBA's ARM7TDMI is 4x faster but speaks a completely different instruction set. The screen is wider (240x160 vs 160x144). Tiles are 4bpp instead of 2bpp. Every aspect must be translated—and the entire CPU core must fit in 32 KB of IWRAM, because accessing EWRAM costs wait states that destroy the 3:1 cycle ratio ChromA needs to keep up with the emulated Game Boy.
The Game Boy's LR35902 has 8-bit registers (A, B, C, D, E, H, L, F) and a 16-bit SP/PC. ChromA maps these onto the ARM's plentiful 32-bit registers, keeping the hot path entirely in registers with no memory loads.
Click a Z80 register to see where it lives on ARM.
Each GB instruction is a handler in IWRAM. The main loop reads one opcode byte from the ROM pointer (mapped GB PC), uses it as an index into a 256-entry jump table, and branches. Each handler does its work, updates the cycle counter, and jumps back.
@ the `fetch` macro (src/gbz80mac.h) -- every handler *ends* with this subs cycles, cycles, #N*CYCLE @ r8 -- charge this instruction ldrplb r0, [gb_pc], #1 @ r9 -- fetch opcode, advance PC ldrpl pc, [gb_optbl, r0, lsl #2] @ r10 -- jump straight to the handler ldr pc, nexttimeout @ budget spent: run the scanline hook
There is no cpu_loop label to return to: this dispatch is
tail-duplicated into the end of every handler, so a finished instruction
jumps directly to the next one with no intervening branch. The pl
condition on the middle two instructions is the whole trick — subs
sets the flags, and if the cycle counter in cycles (r8) has gone
negative the fetch and dispatch are skipped, falling through to
nexttimeout. Checking the budget costs no extra instructions at all.
The GB's flag register (F) has 4 flags: Z, N, H, C in bits 7-4.
The ARM CPSR also has Z and C flags, so ChromA uses native ARM flag operations
for many instructions—an adds sets Z and C natively.
The half-carry (H) and subtract (N) flags have no ARM equivalent and must be
computed manually and stored in the upper bits of the AF register pair.
The GB's 64 KB address space must be mapped onto the GBA's flat 32-bit memory. ROM stays in the Game Pak. Everything else goes into EWRAM buffers, except the hottest state variables which live in IWRAM alongside the CPU core.
Click any region on either side to see the mapping.
When the emulated CPU reads from or writes to an address, ChromA must decode which region it falls in and route it to the correct GBA memory. ROM reads use the GB PC pointer directly into the Game Pak. VRAM, WRAM, and SRAM reads use base pointers into EWRAM buffers. I/O writes are the most complex: each register (LCDC, STAT, SCX, SCY, etc.) has its own handler that may trigger side effects like DMA transfers or PPU mode changes.
@ Example: reading from HL (common pattern) @ r5 = HL, r7 = memory base table mov r0, r5, lsr #12 @ page = addr >> 12 ldr r1, [r7, r0, lsl #2] @ base = table[page] ldrb r0, [r1, r5] @ value = base[addr]
GB tiles are 2bpp with bitplane interleaving: each row of 8 pixels is encoded as 2 bytes. Byte 0 holds the low bit of each pixel, byte 1 holds the high bit. The GBA needs 4bpp linear: 2 pixels per byte, 4 bytes per row.
The trick: CHR_DECODE is a 256-entry lookup table. Each byte (8 bits representing
one bitplane for a row) is expanded to 8 nibbles (32 bits). One lookup per bitplane byte, OR them
together with a shift, and you have 4bpp output. No per-pixel bit twiddling needed.
Consider a row where pixels are: 3, 1, 2, 0, 1, 3, 0, 2.
The bitplane encoding works like this:
Pixel colors: 3 1 2 0 1 3 0 2 In binary: 11 01 10 00 01 11 00 10 Bitplane 0 (low bits): 1 1 0 0 1 1 0 0 = 0xCC Bitplane 1 (high bits): 1 0 1 0 0 1 0 1 = 0xA5 GB stores this as two bytes per row: CC A5 CHR_DECODE[0xCC] expands 8 bits to 8 nibbles. build_chr_decode (lcd.s) puts input bit 0 in the *top* nibble and bit 7 in the bottom one, so the leftmost pixel ends up in the low nibble -- which is where the GBA's 4bpp format wants it: 0x00110011 (pixels, left to right: 1,1,0,0,1,1,0,0) CHR_DECODE[0xA5] expands the same way: 0x10100101 (1,0,1,0,0,1,0,1) Final 4bpp = decode[0xCC] | (decode[0xA5] << 1): 0x00110011 | 0x20200202 = 0x20310213 nibbles, low to high: 3,1,2,0,1,3,0,2 -- the original colors!
Click cells to cycle colors (0-3). Watch how the bytes change.
decode[bp0] | (decode[bp1] << 1) per row
The Game Boy doesn't render its screen all at once. It draws one horizontal line at a time, from top to bottom, exactly like a CRT television. Each of the 154 lines per frame goes through a fixed sequence of phases totaling 456 clock cycles. ChromA must emulate this precise rhythm because games depend on it — they read PPU status registers to know which phase is active and time their VRAM writes around it.
Below, the Y axis is scanlines (0–153) and the X axis is time within each scanline (456 cycles). The 160 pixels of actual screen output are produced only during the Pixel Transfer phase.
ChromA's timeout.s manages the per-scanline state machine. After the CPU
executes enough cycles for one scanline (456 GB cycles ≈ 1,368 ARM cycles at 3:1),
the timeout handler fires. It checks the current LY (scanline counter) and decides
what to do:
pal_before (128 bytes). This frozen snapshot feeds transfer_palette_ at VBlank — insulating PALRAM from mid-frame game writes.ff69_w_tail every 32nd FF69 write when scanline changes.pal_before → PALRAM. Render sprites. Convert dirty tiles. Read/clear pal_scanline_active counter. Arm DMA0-3 for next frame. Set up VCount splits if detected.The 3:1 cycle ratio is critical here: every GB cycle costs ~3 ARM cycles. With IWRAM full (the entire CPU core lives there), there is zero margin for EWRAM wait states in the hot loop.
ChromA translates an entire platform: CPU instructions one-by-one via a jump table, memory through page-table indirection, tiles through lookup-table expansion, and timing through a scanline-locked state machine. Every byte of IWRAM is spoken for.
The source is open. The demo runs in your browser.