PPU

Last updated:

The PPU (Picture Processing Unit, the Ricoh 2C02) is the chip that draws the picture. The CPU never touches the screen directly — it talks to the PPU through eight memory-mapped registers, and everything the PPU does on its own (scrolling, fetching tiles, compositing sprites) runs off a second set of registers the CPU can't address at all.

So there are two register files, and it's worth keeping them apart: the CPU-visible ones the program reads and writes, and the internal ones that hold the rendering state.

CPU-visible registers

Memory-mapped at $2000$2007 and mirrored every 8 bytes up through $3FFF (so $2008 is $2000 again, and so on). The CPU only ever sees the PPU's state through these.

PPUCTRL ($2000, write) — top-level configuration:

bit 7  NMI on vblank   (1 = fire an NMI when vblank begins)
bit 6  master/slave    (unused on the NES)
bit 5  sprite size      (0 = 8x8, 1 = 8x16)
bit 4  background pattern table  (0 = $0000, 1 = $1000)
bit 3  sprite pattern table      (0 = $0000, 1 = $1000, 8x8 only)
bit 2  VRAM increment   (0 = +1 across, 1 = +32 down) per PPUDATA access
bits 1-0  base nametable select  (also written into `t`)

PPUMASK ($2001, write) — what's switched on and how color is filtered:

bits 7-5  emphasize blue / green / red
bit 4  show sprites
bit 3  show background
bit 2  show sprites in the leftmost 8 pixels
bit 1  show background in the leftmost 8 pixels
bit 0  grayscale

Bits 3 and 4 are the master rendering switches; with both off the PPU emits only the backdrop and stops fetching.

PPUSTATUS ($2002, read) — three flags in the top bits:

bit 7  vblank started
bit 6  sprite 0 hit
bit 5  sprite overflow
bits 4-0  stale PPU bus contents (open bus)

Reading it has two side effects: it clears the vblank flag (bit 7) and resets the write toggle w. That second effect is why the usual idiom is to read $2002 before writing a scroll or address — it guarantees the next double-write starts on its first half.

OAMADDR ($2003, write) — the index into OAM (Object Attribute Memory, the 256-byte sprite store) that OAMDATA reads or writes.

OAMDATA ($2004, read/write) — read or write the OAM byte at OAMADDR; writing increments OAMADDR. Games rarely use this directly — OAMDMA is the normal path.

PPUSCROLL ($2005, write twice) — the scroll position, two writes using w: first write is the X scroll, second is the Y scroll. The values land in t (and the fine-X bits in x), not in any register of their own.

PPUADDR ($2006, write twice) — the VRAM address for PPUDATA, two writes using w: first the high 6 bits, then the low 8. It writes into t, then copies t into v. Because it shares t with PPUSCROLL, scroll and address writes step on each other — that overlap is a whole topic of its own.

PPUDATA ($2007, read/write) — read or write VRAM at v, then increment v by 1 or 32 (per PPUCTRL bit 2). Reads are buffered: $2007 returns the previous read and refills the buffer behind it, so a read of normal VRAM is one access stale (palette reads at $3F00+ are the exception and come back immediately).

One more lives on the CPU's side of the bus, not the PPU's, but it's how OAM actually gets filled.

OAMDMA ($4014, write) — write a page number $XX and the CPU copies all 256 bytes of $XX00$XXFF into OAM in one go (513–514 cycles, the CPU stalled throughout).

Internal registers

Not addressable. The CPU nudges these only through the registers above. This is where scrolling and the rendering pipeline actually live.

v (current VRAM address, 15 bits) — the address the PPU is fetching from right now, advancing as it walks tiles across and down.

t (temporary VRAM address, 15 bits) — the staging copy, where PPUSCROLL/PPUADDR writes accumulate; also describable as the address of the top-left on-screen tile. Copied into v at fixed points (all of it on the pre-render line, the horizontal bits at the start of each visible line).

v and t are the two loopy registers, named after the write-up that first documented them. They're called "VRAM address" registers, but their 15 bits are laid out so the same value is the scroll position — which is the trick that makes scrolling cheap (move the camera = change an address):

 yyy NN YYYYY XXXXX
 ||| || ||||| +++++-- coarse X    (tile column, 0-31)
 ||| || +++++-------- coarse Y    (tile row, 0-29)
 ||| ++-------------- nametable select (which of the four)
 +++----------------- fine Y      (pixel row within the tile, 0-7)

x (fine X scroll, 3 bits) — the sub-tile horizontal offset (fineX), the part of the scroll that moves within a single tile.

w (write toggle, 1 bit) — first-or-second-write latch, shared by PPUSCROLL and PPUADDR. Cleared by reading PPUSTATUS.

The rest of the internal state is the rendering pipeline — the registers that turn fetched bytes into pixels. The full mechanics live in their own chapters; here's what each one holds.

The PPUDATA read buffer (8 bits) — backs the one-access-stale read described under PPUDATA: a $2007 read hands back this byte, then the buffer reloads from v.

BG pattern shift registers (two, 16 bits each) — hold the two bitplanes of the tile being drawn. Each is 16 bits rather than 8 because the top half is the current tile and the bottom half is the next tile, already fetched. The pixel emitted each dot is the bit at 15 - fineX, and everything shifts left one place per dot — so over 8 dots a tile clocks out and the next one slides up into place. See the background pipeline.

BG attribute shift registers (two, 8 bits each) — carry the palette bits the same way: one value latched per tile and streamed alongside the pattern bits, so the palette stays in step with the pixels it colors.

The eight sprite units — the sprite-side equivalent, one per sprite chosen for the line. Each holds a pair of 8-bit pattern shift registers, an attribute latch, and an X-position counter that ticks down to the sprite's column before the unit starts shifting out its 8 pixels. Loaded from secondary OAM. See sprite evaluation and the priority mux.