PPU Reference (NES)

Load-bearing facts about the Picture Processing Unit (Ricoh 2C02) for reading NES source code. Curated from nesdev.org/wiki.

Links: NES Research, APU Reference, Mappers Reference, TIA Reference (comparison), Game Annotation Series

The big picture — PPU is not a frame buffer (but kinda)

Unlike the TIA, the NES PPU has its own memory: pattern tables (tile bitmaps), name tables (which tile goes where), attribute tables (palette assignment per region), and OAM (sprite list). The CPU writes into this memory through the PPU’s register interface; the PPU then renders 60 fps without further CPU intervention.

But it’s not a true frame buffer either. The CPU cannot freely write to PPU memory while rendering — VRAM access is gated to vertical blank (~20 scanlines per frame, ~2,273 CPU cycles). And clever programmers learned to write to PPU registers during the visible region to get effects the basic mode can’t produce: mid-frame palette changes, scroll splits (status bar separate from playable area), MMC3 IRQ-triggered raster effects. The beam-racing returns, but as an option for tricks rather than a fundamental requirement.

The chip and its clocks

NTSC frame structure

Region Scanlines Notes
Pre-render 1 (line 261) Dummy line; preps first two tiles
Visible 240 (lines 0–239) Picture; PPU is busy
Post-render 1 (line 240) Idle
VBlank 20 (lines 241–260) CPU’s window for VRAM updates
Total 262 60 fps NTSC

VBlank ≈ 20 × 113.667 = 2,273 CPU cycles per frame for VRAM writes, OAM updates, scroll setup, and game logic. Compare Adventure’s ~5,000 cycles for all game logic — NES has less off-screen time but the PPU does the hard work the TIA didn’t.

Synchronization — the NMI

Unlike the 2600’s 6507, the NES’s 2A03 has interrupts. The PPU asserts NMI at the start of VBlank (scanline 241, dot 1) if PPUCTRL bit 7 is set. The standard pattern:

This is why most NES code structure is “infinite main loop + NMI handler that does the per-frame work.” Compare Adventure’s manually-counted frame structure (chapter 2) — the NMI is doing the bookkeeping the 6507 had to do by hand.

Polling PPUSTATUS for VBlank is unreliable — you can miss it. Always use NMI for frame timing.

PPU registers (CPU-mapped, $2000–$2007 + $4014)

Addr Name R/W Purpose
$2000 PPUCTRL W NMI enable, sprite size, pattern table select, VRAM increment, base name table
$2001 PPUMASK W Background/sprite enable, leftmost-8-pixel masks, color emphasis, greyscale
$2002 PPUSTATUS R VBlank flag (cleared on read), sprite 0 hit, sprite overflow
$2003 OAMADDR W OAM byte index for next OAMDATA access
$2004 OAMDATA R/W Read/write OAM byte at OAMADDR (use OAMDMA in practice)
$2005 PPUSCROLL W ×2 X scroll then Y scroll (twice-write latch)
$2006 PPUADDR W ×2 High byte then low byte of VRAM address (twice-write latch)
$2007 PPUDATA R/W Read/write VRAM at current address; auto-increments
$4014 OAMDMA W Source page (high byte) for fast 256-byte OAM upload

PPUCTRL ($2000) bit layout — VPHB SINN

PPUMASK ($2001) bit layout — BGRs bMmG

Common idiom: write $00 to disable rendering during long VRAM transfers; write $1E (0001 1110) to re-enable both sprites and background with no leftmost mask, no color emphasis, no greyscale.

PPUSTATUS ($2002) bit layout — VSO- ----

Reading PPUSTATUS clears the internal w write-latch (shared with PPUSCROLL and PPUADDR). Standard NMI-handler opening is LDA $2002 to reset the latch.

The twice-write latch (PPUSCROLL / PPUADDR)

Both share an internal flip-flop w. First write goes to one latch, second to the other; the latch toggles. Any read of PPUSTATUS resets w to 0. This is the PPU footgun:

Scrolling (the v/t/x/w internal model)

The PPU has internal registers v (current VRAM address, 15 bits), t (temporary VRAM address), x (fine X scroll, 3 bits), w (write-toggle, 1 bit). Writes to PPUSCROLL/PPUADDR/PPUCTRL update t and x; certain events copy t into v.

Scrolling is “set t for next-frame target, let v get copied at the right moment.” Mid-frame scroll splits (e.g., status bar that doesn’t scroll) work by carefully writing PPUSCROLL or PPUADDR mid-frame to override v. The famous v <- t copy at dot 257 of pre-render lets you split the screen.

OAM and OAMDMA

Sprite limits

Pattern tables and name tables

The pattern tables are CHR-ROM (or CHR-RAM) — for simple mappers like NROM, this is an 8 KiB chip on the cartridge. Mappers add bank-switching to swap tile sets mid-game.

Sprite 0 hit — the timing primitive

PPUSTATUS bit 6 sets when a non-transparent pixel of sprite 0 overlaps a non-transparent background pixel. This is detectable by the CPU and is the standard way to trigger a mid-frame event at a specific scanline without needing a mapper IRQ. Status-bar games place a small invisible sprite at the boundary; the CPU spins reading PPUSTATUS until bit 6 sets, then writes new scroll values for the post-status-bar region.

VRAM access window (the actual constraint)

Direct PPUDATA writes work cleanly only when rendering is off (PPUMASK bits 3–4 cleared) or during VBlank. Writing during the visible region produces garbage and “glitchy increments” of OAMADDR.

So the practical pattern:

  1. NMI fires at start of VBlank.
  2. NMI handler does OAMDMA, then PPUDATA writes (palette updates, name table patches), then scroll setup.
  3. RTI before VBlank ends (~2,273 cycles total budget).
  4. Main loop runs game logic until next NMI.

Cycle budget per frame (NTSC)

Compare Adventure: ~5,000 cycles for game logic per frame. NES has 5–6× more CPU time for game logic because the PPU does the rendering work the TIA forced the CPU to do by hand. This is the capability shift that lets NES games carry richer state machines, scripted events, and longer sequences — the storytelling-vs-logic axis.

Comparison to TIA at a glance

Aspect TIA (2600) PPU (NES)
Memory None — CPU writes registers in real time 8 KiB CHR-ROM/RAM + 2 KiB name table RAM + 256 B OAM
Sprites 2 player + 2 missile + 1 ball 64 sprites OAM, 8 per scanline
Sprite size 8 wide × variable 8×8 or 8×16, configurable
Color 1 per sprite + playfield 4 palettes × 4 colors per sprite, configurable per 16×16 region
Scrolling None — every pixel is hand-drawn Hardware scrolling with v/t/x/w
Frame sync WSYNC + manual cycle counting NMI at VBlank
Mid-frame tricks Required for any non-trivial graphics Optional, for scroll splits / palette tricks

The TIA forces beam-racing; the PPU rewards it (when used by experts). Same chip family, totally different relationship between CPU and graphics hardware.

Tags

6502 · reverse-engineering · assembly · nes