Skip to content
RetrogamingDeep Dive Published Updated 6 min readViews unavailable

Game Boy Color Palette RAM: Indexed Access and Mode 3 Traps

Trace CGB background and object palette writes through index registers, RGB555 storage, auto-increment, and the PPU's access window.

A Game Boy Color pixel does not contain a finished RGB value in tile memory. The tile supplies a two-bit color index; an attribute selects one of eight background or object palettes; palette RAM supplies the color. An emulator can decode the tile correctly and still show the wrong picture if it conflates these stages, applies a monochrome register in CGB mode, or lets palette writes occur at the wrong PPU time.

Pan Docs describes two separate 64-byte palette-RAM banks: one for background/window and one for objects. Each holds eight palettes of four 16-bit entries. The color format is RGB555, stored little-endian; the upper sixteenth bit is ignored for rendering. Object color index zero is transparent even though it occupies storage. Background color zero is an actual palette entry, so treating all zero-index pixels as transparent would produce missing backgrounds.

Follow the indexed access path

Palette RAM is not directly mapped as ordinary CPU memory. Background index register BGPI ($FF68, also called BCPS) selects a byte address; BGPD ($FF69, or BCPD) reads or writes the selected byte. Object registers OBPI ($FF6A) and OBPD ($FF6B) provide the same mechanism for the separate object bank. The index register’s low six bits select one of 64 bytes; bit seven enables increment after a data write.

For example, writing background palette zero’s first two colors begins with index zero and sends four bytes in low-byte/high-byte order. With auto-increment enabled, each successful data-register write advances the index. A read does not advance it. A write attempted while palette RAM is inaccessible still advances the index, even though the color byte is not stored. That final detail is a useful emulator test: suppressing the write but forgetting the index side effect desynchronizes later colors.

Do not confuse the selector with the color value. The address points to bytes, not to a palette number; palette n starts at byte 8n. The background and object address counters are independent. A write to BGPD must not mutate object palette RAM, and a transition between palette seven and palette zero wraps the six-bit byte address.

Respect the PPU access window

The PPU’s Mode 3 pixel transfer reads palette RAM. During that phase, CPU reads through BGPD or OBPD return $FF and writes are ignored, according to Pan Docs. The index registers themselves remain accessible. Modes 0, 1, and 2 permit palette RAM access; Mode 3 does not. An implementation that simply gates all four registers together gets this wrong.

Timing matters in homebrew and emulation. A game may upload colors during VBlank or HBlank to avoid Mode 3 contention. A mid-frame palette effect can rely on the exact point when the PPU changes mode. For ordinary asset loading, write palettes when the LCD is off or in a known safe window; for an emulator, model the access rule even when most games appear to work without it.

Verify colors and display conversion separately

First test storage: write known RGB555 words to both banks, read them back in an accessible mode, verify byte order, auto-increment, wrapping, and isolation. Next test pixel selection: use distinct colors in all eight background palettes and object palettes, then change only the tile-map or OAM palette attribute. Finally test the Mode 3 access rule, including the ignored-write/index-increment combination.

The RGB555 word is a hardware code, not a promise of identical appearance on every display. Pan Docs notes that the original unlit CGB LCD and a modern sRGB monitor do not render the same code identically. Keep palette-register correctness separate from an optional color-correction profile. This prevents a visual filter from masking a byte-order or timing bug.

Work through one byte-level example

Suppose a test wants background palette zero, color zero to contain the RGB555 word $001F and color one to contain $03E0. Because the two-byte entries are little-endian, the palette data bytes at addresses zero through three are $1F, $00, $E0, $03. Select byte address zero in BGPI with auto-increment enabled, then send those four bytes to BGPD in order. A debugger should show the index advanced to four and those exact bytes in background palette RAM. Do the equivalent operation through OBPI/OBPD and confirm the background bank has not changed.

This example is a register-contract test, not a recommendation to write palette bytes at arbitrary times. If the second byte falls in Mode 3, the hardware ignores it but still advances the selected address. A naive emulator might instead store it, or might ignore both the store and the increment. Either defect moves every subsequent color differently. Make a unit test with a controlled PPU mode transition between writes and assert both the resulting RAM and final index.

The index register has another asymmetry: writing or reading the selector itself is permitted outside VBlank/HBlank, even when the data register is blocked in Mode 3. A test should set the selector during Mode 3, read it back, then access data in a permitted mode. An emulator that blocks the selector because it shares the same palette subsystem would fail despite producing many ordinary game screens correctly.

Distinguish compatibility mode from CGB mode

Pan Docs separates the monochrome BGP, OBP0, and OBP1 registers from CGB palette RAM. In non-CGB mode, those registers map two-bit pixel indices to grayscale choices; object index zero remains transparent. In CGB mode, palette choices come from CGB palette memory and attributes. An emulator needs to decide the hardware mode from cartridge and boot behavior before choosing the color path. Applying BGP to native CGB tile output, or assuming CGB palette RAM determines every monochrome game’s shades, mixes two distinct rendering models.

Initial palette contents also matter for tests. Pan Docs says the CGB boot ROM initializes background colors to white but leaves most object colors uninitialized. Therefore a test ROM that never writes its object palettes cannot demand one deterministic object color from all hardware states. A reproducible rendering test should initialize every palette entry it observes and should not infer an emulator bug solely from startup colors that software did not define.

Finally, avoid treating “four colors per object palette” as four visible sprite colors. Object color index zero is the transparent sentinel, so at most three entries contribute visible object pixels at one instant. This distinction is essential for a palette viewer: it can show the stored fourth word without claiming that color zero paints an opaque sprite pixel.

Related:

Sources:

Comments