Writing an NES emulator — one page

Last updated:

My working notes on building an NES emulator.

Intro

Who this is for

This is for developers that want to build an NES emulator. It assumes you're comfortable programming — it isn't the place to learn your first language, and the code is left to you.

Being new to emulation is fine, though; that's part of what the milestones below are for.

It is not an electrical-engineering text. Hardware details — latches, dummy reads, an address sitting on the bus for a cycle — are described only when an emulator has to reproduce them to accurately match the system's behaviour; if something has no observable consequence, it isn't here. So you don't need a hardware background to follow along.

Scope and status

Work in progress, started June 2026. I'm writing these pages as I work through the system myself, so they land incomplete, get rewritten, and occasionally contradict each other for a while. Cross-check anything against NESdev before betting code on it.


Notation

A couple of conventions that show up everywhere in the rest of the book.

Hexadecimal

$10 and 0x10 is hexadecimal for the same number, 16 in decimal.
In this book I prefer $<number> since it's one character instead of two.

In most low-level programming, $10 is an address (decimal 16). Here it can be an address or a value — it depends on context.

0x is the C-style prefix; both just mean "read what follows as hex". In a language like C or Zig you'd write:

  int value = 0x10; // C
  const value = 0x10; // Zig

0-based vs 1-based

Mostly for beginners.

When we say "byte 0," we mean the first byte, and viceversa.

A common snag:

Assume first means byte $0 unless stated otherwise — though some things really are 1-based. It's annoying at first, but you get used to it.


Roadmap

This chapter is a list of milestones to aim for while building the emulator. The rest of the book follows the order you'd actually build things in, while still working as reference.

Especially if it's your first emulator, I think momentum increases the chances of finishing it.

Every milestone ends in something you can verify. The concepts get explained, but each one closes with concrete work that proves your code actually does what it should.

Here's the milestones:

  1. Draw the graphics stored in a cartridge. Pull the art straight out of the ROM and onto the screen, before implementing any chips — so you see something early.

  2. Diff against nestest. Build the CPU and check it against nestest, a test ROM with a reference log — so you add opcodes incrementally instead of all at once.

  3. Diff against Mesen. This is pretty much the same as nestest, except we can pick any ROM we want, and we compare our log to that of Mesen (a mature, well-tested NES emulator), while both run, line by line.

  4. Render one of the nametables in real time, in grayscale. A nametable (also called the name table) is the grid of tiles that makes up a background screen. Instead of dumping one frame to a file like in milestone 1, put it in a window and redraw it every frame as the game runs — still grayscale, no palette. This is where the emulator starts to feel live.

  5. Render the background, for real. The same screen as milestone 4, but built the way the console builds it: color from the palettes, horizontal and vertical scrolling, and the frame produced one pixel at a time by the background pipeline instead of read straight off the nametable. Still no sprites — just the world, in color and in motion.

  6. Sprites. The up-to-64 movable tiles the PPU calls objects: sprite evaluation and the 8-per-scanline limit, then getting them on screen in the right place, palette, and flip. Drawn over the backdrop on their own, so you can check the sprite system without the background tangled in.

  7. Composite the background and sprites. Run both pipelines together and pick one pixel per dot — the front-or-behind priority that lets a sprite pass behind the scenery. Now sprites and the background share one screen the way they do in the game.

  8. Sprite 0 hit. The flag the CPU polls to learn the beam has reached a known sprite — the classic mid-frame split, a static status bar over a scrolling playfield. The flag itself is a few lines; the real work is that the PPU and CPU now have to keep close enough time for a game to trust it. This is the first place rendering drives game logic.


Milestone #1: Basic graphics

The first milestone is to pull the art straight out of a cartridge and put it on screen — before any of the emulator components are implemented.

It's nice to see something — anything — early, instead of taking it on faith that the guide will eventually get you to pixels. A cartridge carries its graphics as plain data, so you can decode and display them without emulating a single instruction.

What it takes

Three things, each its own chapter:


Cartridges and ROM files intro

What's in a cartridge

An NES game ships on a cartridge: a small circuit board with one or two ROM chips on it, and sometimes a bit of extra hardware.

But emulators do not have access to a physical cartridge, instead what they read is a ROM file (also called a ROM image, or just a dump).

A ROM file is a copy of a cartridge chips' contents written out as one file.

But since cartridges have multiple chips, that means there's no single way to store their contents. E.g. with chips A and B, you could store chip B's contents first, then chip A's.

To standardize this, emulator devs propose different formats to organize this data, and usually one or two become widely popular and de-facto.

One such format is:

iNES

Almost every dump you'll come across is in the iNES format, named after the early emulator of the same name; the file extension is .nes.

Its successor, NES 2.0, is a backward-compatible extension of the same layout — for the fields we touch here, code that reads iNES reads NES 2.0 too.

The layout is just four parts, in order:

Everything after the header is raw chip data with nothing between the sections — no offset table, no markers. You locate a section by adding up the sizes of everything before it, and those sizes come from the header.

The header

The header is the first 16 bytes of the file.

Here's the actual header of Thwaite, a homebrew NES game game by Damian Yerrick, released under GPLv3. I'll use it throughout the book.

byte   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
value  4E 45 53 1A 01 01 01 00 00 00 00 00 00 00 00 00
       └────┬────┘  │  │  │  │
       "NES\x1A"    │  │  └──┴ Flags (we'll see this later)
        (magic)     │  └──── CHR-ROM size: 1 × 8 KiB
                    └─────── PRG-ROM size: 1 × 16 KiB

The bytes that matter, read against Thwaite's values:

Bytes 0–3 — the magic number NES\x1A. If it's not there, the file isn't iNES; stop parsing.
Thwaite: 4E 45 53 1A

Byte 4 — PRG-ROM size in 16 KiB units. You don't run this code to draw graphics, but you need its size to know where CHR-ROM begins.
Thwaite: 01 → 1 × 16 KiB

Byte 5 — CHR-ROM size in 8 KiB units. 0 means the cartridge uses CHR-RAM and has no tile data in the file.
Thwaite: 01 → 1 × 8 KiB

Byte 6 and 7 — Flags that will explained later on, when they are necessary for progress.

Finding the CHR-ROM of a ROM... and why?

Knowing the address and length of CHR-ROM is central to the following chapter.

CHR-ROM starts right after PRG-ROM. Its start and length both come from the header:

chr_start = 16 + (trainer ? 512 : 0) + prg_16k_units * 16384
chr_len   = chr_8k_units * 8192

Plugging in Thwaite — no trainer, one 16 KiB PRG bank, one 8 KiB CHR bank:

chr_start = 16 + 0 + 1 * 16384 = 16400  ($4010)
chr_len   =          1 *  8192 =  8192  ($2000)

So CHR-ROM spans from $4010 to $600F.

That 8 KiB slice is the pattern-table data — the input to the next chapter, where we decode it and put it on screen.

Cart and .nes file

Both terms are used interchangeably.


Pattern tables

The majority of NES game graphics are stored in Tiles within structures called Pattern Tables (sometimes called the tile tables).

These tiles are similar to the spritesheets you'd find in a modern 2D game: a sheet of small images for game graphics.

These pattern tables are stored in the CHR-ROM memory of the cart, location of which is explained in the last part of previous chapter.

Next are displayed the pattern tables of the Thwaite game. Since:

... there's two pattern tables in Thwaite:

Pattern table0 ($4010-$500F):

Thwaite pattern table 0, 256 tiles in a 16×16 grid: a full font and a row of little houses

Pattern table1 ($5010-$600F):

Thwaite pattern table 1, 256 tiles in a 16×16 grid: explosions, missiles, and detailed houses

Keep in mind, each pattern table is displayed in a 16×16 tiles layout, left to right, top to bottom. But this is just to fit in the webpage width.
The tiles are actually stored as a "single strip", one after the other

Why is it in black and white?

Colors are not stored in pattern tables (why is explored at a later chapter).
Because of that, we rendered these in grayscale.

But even without the palette, we can discern the shapes: the font, the houses you defend, the missiles and explosion bursts, and the chunky title-screen letters spelling thwaite.

How is every tile represented in the data?

To teach how tiles are stored, I'll use the 1-tile little house below the o and right to the ~ tiles from pattern table 0.

This is the house scaled up:

The decoded tile rendered in grayscale: a small house

Notice how there's only 4 different grayscale tones, this will be important later.

We're gonna first obtain the 16 bytes of this tile and then we're going to convert them, somehow, into this image.

Getting the tile bytes address

To obtain those 16 bytes, we first need their address.

This tile is the tile 127 (0-based) counting left to right (row 7, column 15 so 7*16 + 15 = 127).

And each tile takes 16 bytes, so 126*16=2032, in hex 0x7f0

Since the pattern table starts at 0x4010 + 0x7F0 = 0x4800

We can get the 16 bytes at 0x4800 with xxd:

> xxd -s 0x4800 -l 16 thwaite.nes
20 00 00 0a 7a fb ff 00 ┊ 24 2e ff f1 04 a5 a5 00

Rendering the image

The first 8 bytes of the tile are bitplane 0, and the latter 8 are bitplane 1. And we're going to show every byte in binary.

        bitplane 0 (low bit)         bitplane 1 (high bit)
row 0   $20  0 0 1 0 0 0 0 0      $24  0 0 1 0 0 1 0 0
row 1   $00  0 0 0 0 0 0 0 0      $2E  0 0 1 0 1 1 1 0
row 2   $00  0 0 0 0 0 0 0 0      $FF  1 1 1 1 1 1 1 1
row 3   $0A  0 0 0 0 1 0 1 0      $F1  1 1 1 1 0 0 0 1
row 4   $7A  0 1 1 1 1 0 1 0      $04  0 0 0 0 0 1 0 0
row 5   $FB  1 1 1 1 1 0 1 1      $A5  1 0 1 0 0 1 0 1
row 6   $FF  1 1 1 1 1 1 1 1      $A5  1 0 1 0 0 1 0 1
row 7   $00  0 0 0 0 0 0 0 0      $00  0 0 0 0 0 0 0 0

Now, merge both planes into a new grid. For each pixel, stack its high bit (plane 1) over its low bit (plane 0) to get a 2-bit number:

merged (high bit, low bit)
row 0   00 00 11 00 00 10 00 00
row 1   00 00 10 00 10 10 10 00
row 2   10 10 10 10 10 10 10 10
row 3   10 10 10 10 01 00 01 10
row 4   00 01 01 01 01 10 01 00
row 5   11 01 11 01 01 10 01 11
row 6   11 01 11 01 01 11 01 11
row 7   00 00 00 00 00 00 00 00

Now read each 2-bit number as a value 03:

. . 3 . . 2 . .
. . 2 . 2 2 2 .
2 2 2 2 2 2 2 2
2 2 2 2 1 . 1 2
. 1 1 1 1 2 1 .
3 1 3 1 1 2 1 3
3 1 3 1 1 3 1 3
. . . . . . . .
The decoded tile, a small house

Hopefully, via intuition, you'll see how every number matches the tile. And that's how the tiles are rendered from data.

But where's the color?

Color is determined from a selected palette, which is determined at runtime. We're gonna see that way later.


Checkpoint #1: Draw the graphics stored in a cartridge

You know what pattern tables are, where they live in the ROM, and how a tile's bytes turn into pixels. Now let's put a whole pattern table on screen.

The easiest way to do that is rendering it into an image file.

PPM, the easiest format

We need to write an actual image file, and the least painful one to produce by hand is PPM (portable pixmap, part of the Netpbm family alongside PGM for grayscale and PBM for black-and-white).

There's no library to pull in: a PPM is a tiny text header followed by pixel values. The ASCII variant (P3) looks like this:

P3
4 2
255
255 0 0   0 255 0   0 0 255   255 255 255
0 0 0     0 0 0     0 0 0     0 0 0

That's a 4×2 image: a red, green, blue and white pixel on top, a black row below. There's also P6, the same thing with the pixel data as raw bytes instead of ASCII — smaller files, write that once the ASCII version works.

Notice how this format is just plaintext (not binary), and how 1 or more whitespaces are used as separators between the colors. That's why it's so friendly.

We're still in grayscale (color comes later), so map the four pixel values to four gray triples.

If RGB is new to you: each pixel is three numbers — red, green, blue — each from 0 to 255. When all three are equal you get a shade of gray, from 0 0 0 (black) up to 255 255 255 (white).

So pick four evenly-spaced grays:

The goal, and the obstacle

We want the table laid out as a 16×16 grid of tiles, like the images in the previous chapter: 256 tiles, 8 pixels each, so a 128×128 image.

The obstacle is interleaving. PPM is written one full pixel row at a time, left to right, top to bottom. But tiles are stored as a strip, one complete tile after another. So the image's first pixel row isn't the first tile — it's row 0 of tile 0, then row 0 of tile 1, … up to row 0 of tile 15, then the image's second row is row 1 of those same 16 tiles, and so on. To emit the file in order you'd have to hop between 16 tiles per scanline. That bookkeeping is where it gets error-prone.

Build up to it

Don't fight the interleaving head-on. Add it last, one step at a time, so a bug has only one new thing it could be:

  1. One tile. Render a single 8×8 tile to an 8×8 PPM. No layout problem at all — you're just checking your bitplane decode produces the right pixels.

  2. A single column. Stack tiles vertically: an 8-wide, 8·N-tall image. This is the case worth noticing — when the image is one tile wide, a pixel row is a tile row, so writing the file in order is the same as walking the tiles in order. No interleaving yet, and you've confirmed you can place many tiles.

  3. More columns. Now go to the real grid. This is the step that introduces interleaving, and the clean way to handle it is to stop writing pixels straight to the file. Decode into an in-memory framebuffer — a flat width × height array — and blit each tile into its (x, y) slot, then dump the whole buffer at the end. With random-access writes the strip-vs-grid mismatch disappears; serializing to PPM becomes a dumb loop that doesn't care how the pixels got there.

Doing it in this order means by the time you hit the grid, the only thing left to get wrong is the tile placement arithmetic — everything before it is already proven. It's slower to write but it makes the inevitable trial-and-error quick to pin down.

Blitting

Blit (short for bit block transfer, also written blitting) is the move in that last step: copy a small rectangle of pixels into a bigger buffer at some position. One tile is one blit.

Drawing the whole image is just that, in a loop — stamp each tile into its slot until the framebuffer is full:

Four stages side by side: an empty 4x2 tile grid, then the two roof tiles filled in, then the whole house, then the full house-and-tree scene

Nothing special happens between tiles. Each blit writes its own 8×8 block and the next one writes the block beside it. Here's a single blit on its own, with the destination slot it lands in outlined:

A 4x2 framebuffer, mostly empty, with one tile copied into a single highlighted slot

The source can be any rectangle and you choose where it goes — copying one image into another at a position is a basic move you'll reach for constantly, not just here.


Milestone #2: Basic CPU

Before this one: from here on we're imitating the hardware, not yet understanding it. If that framing bothers you, I wrote about why it has to be this way in Imitate first, understand later.

The second milestone is a working 6502 CPU — the processor at the centre of the NES.

What it takes


CPU Introduction

The NES CPU is a Ricoh 2A03 — a 6502 with no decimal mode. Each cycle below is one memory access on the bus; the CPU never sits idle, so even "do nothing" steps show up as discarded reads.

CPU flow

The CPU is built to execute instructions sequentially, one by one, by reading them from memory. An instruction is 1, 2, or 3 bytes long, and the first byte is always the opcode ("operation code") — a one-byte number that identifies the instruction.

Addresses on this CPU are 16 bits long (2 bytes) — that's a 64 KiB address space, $0000 to $FFFF.

The CPU tracks where it's reading from with a 16-bit register called PC (program counter): the address of the next byte to read. The basic operation read [PC]; PC++ is called a fetch. Executing an instruction is a sequence of fetches — first the opcode, then the remaining bytes (if any).

Cycle 1 of every instruction is therefore the same: fetch the opcode to identify the instruction.

So the whole CPU is a loop: fetch an opcode, do whatever it says, repeat.

loop {
    opcode = read(PC); PC++   // cycle 1: fetch the opcode
    execute(opcode)           // the rest of the cycles depend on the instruction
}

execute is empty for now — filling it in, one opcode at a time, is the work of the checkpoint. Everything else on the CPU pages is detail that hangs off this loop.

Example

Say PC = $8000 and memory contains:

$8000   C9
$8001   42
$8002   ...

The CPU is about to start a new instruction. It fetches the opcode:

cycle 1   read [$8000] → C9       (opcode); PC = $8001

C9 is the opcode for an instruction that does something specific with one operand byte (what exactly isn't important here). The CPU fetches the operand:

cycle 2   read [$8001] → 42       (operand); PC = $8002

PC sits at $8002, ready for the next opcode fetch.


CPU State

Below is a list of parts of the CPU that hold a value in execution, The reader needs to include these into whatever class that represents the CPU.

(If your programming language requires defining as signed or unsigned, use unsigned)

A (accumulator, 8 bits) — main scratch register; target of most ALU operations.

X, Y (8 bits each) — index registers; used by indexed addressing modes and as loop counters.

PC (program counter, 16 bits) — address of the next byte to read. Split into PCL (low 8 bits) and PCH (high 8 bits); the cycle breakdowns refer to them separately when the CPU updates one half without the other.

SP (stack pointer, 8 bits) — low byte of the stack address. The stack lives in page 1, so the full address is 01:SP. Push decrements SP, pull increments it.

P (status register, 8 bits) — the flags:

bit 7 6 5 4 3 2 1 0
    N V - B D I Z C

N (negative), V (overflow), B (break — only meaningful in the stacked copy pushed by PHP/BRK), D (decimal — ignored by the 2A03), I (interrupt disable), Z (zero), C (carry).

IR (instruction register, 8 bits) — holds the opcode of the instruction currently executing. Loaded at cycle 1.

AD (effective address latch, 16 bits) — where the current instruction's operand address is assembled during addressing. Split into ADL (low) and ADH (high). The lo and hi you'll see in the mode breakdowns are ADL and ADH being written and later read as [ADH:ADL].

DL (data latch, 8 bits) — holds the most recent byte read from the bus; used to carry values between cycles.

"Fake" latches

The following latches exist in some form in the real hardware but are oversimplified here — though enough for accurate emulation. Include them too.

CL (carry latch, 1 bit) — remembers the page-crossing carry bit between the cycle that adds an index to the low address byte and the cycle that does the fixed-address re-read.

NEGATIVE (sign latch, 1 bit) — remembers the sign of a value across cycles, mainly for relative branches whose offset is negative.

IAL (intermediate ALU latch, 8 bits) — parks an ALU result between cycles so the next bus transaction can consume it.


Checkpoint #2: Match nestest

The 6502 has a lot of opcodes, and writing them all before running anything is a long stretch with no feedback. nestest removes that. You make your emulator print its state in the same format, run it alongside the log, and stop at the first line that doesn't match — that line points straight at the bug.

So you build the CPU incrementally: implement an opcode, see how much further you get before the next mismatch, implement the one it tripped on, repeat. You're never debugging more than one instruction at a time.

The trap with a CPU is reading the whole instruction set before running anything. Don't. The point of nestest is that the failing log is the to-do list: it tells you which opcode to write next, one at a time. So we build the smallest CPU that runs, point it at nestest, and let it tell us what's missing.

The loop itself is small. Every opcode is unimplemented to start with:

loop {
    log_upcoming_instruction_and_state()
    opcode = read(PC); PC++
    switch (opcode) {
        // nothing here yet
        default: panic("unimplemented opcode {opcode:02X} at {PC-1:04X}")
    }
}

First rung: match line 1, with zero opcodes

nestest runs in automated mode: you force PC = $C000 at startup instead of using the reset vector, and the rest of the state is the standard reset — A=X=Y=00, P=24, SP=FD, and the cycle counter already at 7 (the reset sequence burns seven cycles). So the first line of the log is:

C000  4C F5 C5  JMP $C5F5                       A:00 X:00 Y:00 P:24 SP:FD CYC:7

(The 4C F5 C5 bytes and the JMP $C5F5 disassembly are a separate concern: printing them means knowing each instruction's length, which only falls out once you decode opcodes. Add those columns as you go, or leave them off the diff at first — they're for humans, not for matching.)

The development loop

Run it against nestest.log and diff. With the skeleton, line 1's state matches, then the CPU fetches 4C and panics: unimplemented opcode 4C. That's your instruction:

  1. Look up 4C in the instruction reference — it's JMP absolute. Implement just that one.
  2. Rerun. The CPU now executes the jump, logs line 2, and panics on its opcode.
  3. Repeat. Implement the opcode it tripped on, see how much further you get.

You're never holding more than one instruction in your head. When the log stops mismatching, you've matched nestest — and the CPU is the part you can now trust.


CPU Instruction Guide

The full opcode table lives in the CPU Instruction Reference. This page explains how to read it.

Don't read it top to bottom because checkpoint #2 sends you there one opcode at a time.

There's 256 instructions, and they are identified by a onebyte opcode (0x00 to 0xff). Each instruction has its own fixed behavior.

Most instructions are a unique pair of mnemonic, addressing mode: Other instructions are mnemonic. The opcode byte just encodes that pair. LDA $42 (opcode A5), zero page and LDA #$42 (opcode A9) are different instructions, they have the same mnemonic (LDA), but different addressing modes.

The addressing mode describes how a specific value is obtained. The mnemonic usually describes what to do with that value.

Special instructions

There's a small group of unique instructions that don't have an addressing mode. They do unique work and I call these special:

Operands

Instructions can have 0, 1 or 2 operands. The importance of this is just for matching nestest, because the format... (complete this)

Illegal instructions

Also known as undocumented instructions in official docs. Many (most?) games make use of these instructions, so they are included here. and the genuinely unstable ones carry a warning.

Flags

Flags are shown as N V - B D I Z C: a letter means the instruction sets that bit, - means it's preserved. N and Z track the result — the last value the instruction computes (the register it loads, the byte it writes, or its final register update), which isn't always the output. C and V, when touched, are spelled out in the entry.

Addressing modes

Addresses are written high:low when assembled across cycles — typically ADH:ADL for the operand address being built, or PCH:PCL for the program counter. Cycle 1 (opcode fetch) is omitted from every mode below.

Access type. A memory mode doesn't pin down the cycles on its own: it works out where the operand is, but what happens at that address depends on the instruction's access type:

Each instruction below is tagged with its access type. The memory modes share their address-calculation cycles and then branch on it, shown as Access type: read / write / rmw. Two things to notice in the branch:

Implied, Accumulator, and Immediate have no memory operand to branch on, so their single form is listed as-is — Accumulator is an RMW on A with no bus access.

Opcodes in each group are listed ascending by value.

Dummy reads

Each cycle is written as assignments, Zig-style. bus.read(addr) is a read bus cycle returning the byte at addr; bus.write(addr, val) is a write cycle. _ = bus.read(addr) is a dummy read: the CPU still drives the cycle on the bus, it just throws the byte away (_ discards it, as in Zig). The hardware can't skip these. // starts a comment.


Chip timing and the emulator loop

The NES has a few different chips — CPU, graphics (PPU), audio (APU) — and they all share a single source of time: a crystal oscillator on the board that sends out a pulse every ~47 nanoseconds. The stream of pulses it produces is called the master clock, and one pulse is one clock (or tick, or cycle).

Every chip is wired to the same pulse stream, but one cycle of work for each chip takes a different number of clocks to complete:

The chips run at different cycle rates, but they are all in-sync to the same master clock.

Aside on vocabulary: in NES and 6502 documentation you'll see clock, tick and cycle sometimes used interchangeably to mean "one beat of some chip's clock." I prefer to use the terms "master clock" and "cpu cycle", "ppu cycle", but it's not a hard rule. Also, throughout these notes I count clocks and cycles starting at 1, not 0 — so the first clock of a CPU cycle is clock 1, the last is clock 12.

Coding the parallelism of chips

On the real console the chips run in parallel. In an emulator running on a modern CPU we can't reproduce that — using one thread per chip wouldn't make sense because the OS scheduler's has jitter, it can't be made to tick at exact intervals.

But we don't need real-time parallelism. What makes the emulation accurate is the order in which the chips tick in relation to each other.

There's two ways I noticed this can be coded:

Chip Parallelism #1: CPU-centric

First, let's define what a CPU step is. It is one full CPU operation — either an instruction (2–7 cycles) or an interrupt service sequence (7 cycles). A CPU step is several CPU cycles long; a CPU cycle is 12 clocks.

Why multiple cycles per step? Because every read or write the CPU does on the bus takes one full cycle, and an instruction is defined as a sequence of bus accesses. LDA $1234 runs as:

  1. Read the opcode.
  2. Read the low byte of the address.
  3. Read the high byte of the address.
  4. Read the value at $1234.

Four bus accesses, four cycles. The cycle count of an instruction is essentially "how many times it touches the bus."

A simple but incorrect first attempt:

while (true) {
    cycles = cpu.step();         // run one operation; returns # cycles (2–7)
    ppu.tick(cycles * 3);        // 3 PPU cycles per CPU cycle
    apu.tick(cycles);            // 1 APU cycle  per CPU cycle
}

The arithmetic is right — each chip gets the correct total amount of catch-up — but the order is wrong. The loop runs the CPU's entire step in one shot, then the PPU, then the APU. For a 2-cycle step it looks like this:

order of work:

     CPU:  ▓ ▓
     APU:      ▓ ▓
     PPU:          ▓ ▓ ▓ ▓ ▓ ▓

But, on the real console the three chips are running side by side, ticked off the same master clock. Each chip's cycle is a span of master clocks (12 for CPU and APU, 4 for PPU), and the spans overlap in time:

clocks:  0   4   8   12          24
   CPU:  ├───────────┼───────────┤    2 CPU cycles (12 clocks per cycle)
   APU:  ├───────────┼───────────┤    2 APU cycles (same as CPU)
   PPU:  ├───┼───┼───┼───┼───┼───┤    6 PPU cycles (4 clocks per cycle)

This is a good proof of concept of the interleaving we want.

Reproducing this interleaving is what emulation accuracy hinges on: the chips affect each other through the bus, and when a write or read lands relative to the other chips' progress changes what they observe. Not interleaving the order of chip ticks makes emulation diverge from real hardware.

To achieve this, every time the CPU does a bus access (which is once per cycle), it should clock() 12 times before moving on, because a CPU cycle takes 12 clocks:

fn cpu.step() {
    // ... while running the operation, for each cycle:
    bus.read(addr);              // bus.read internally calls tick_others()
}

fn bus.read(addr) {
    clock(12);
    const byte = read_byte(addr);
    return byte;
}

fn clock(n) {
    for (0..n) {
      master_clocks += 1;
      if (master_clocks % 4 == 0) ppu.tick(3);
      if (master_clocks % 12 == 0) apu.tick(1);
    }
}

Actually accurate cycle emulation

The former explanation was simpler for learning purposes, but for the emulation to be accurate the graph would be more like this:

clocks:     3   7   11  15  19  23    
   PPU:  ───┼───┼───┼───┼───┼───┼─
   CPU:  ├────R──────┼──────W────┤
clocks:  0    5      12     19   24
              R = read (early, clock 5/12)
              W = write (late, clock 7/12)

We introduced two real hardware details here:

PPU/CPU phase offset. The PPU's ticks don't align with the CPU's cycle boundaries — above they fire at clocks 3, 7, 11, …, not 4, 8, 12, …. The offset is one of three values picked randomly at power-on; I use -1. In clock function:

const ppu_offset = -1;
fn clock(n) {
  ...
  if ((master_clocks + ppu_offset) % 4 == 0) ppu.cycle();
  ...
}

Read vs write timing. The CPU's bus access lands at a specific master clock inside the 12-clock window with an offset altered by the current CPU cycle being a read or a write.

Above, cycle 1, which is a read, gets the value in clock 5, but cycle 2, which is a write, happens at clock 7 (late).

Reflected in bus.read / bus.write:

fn bus.read(addr) {
    clock(5);
    const byte = read_byte(addr);
    clock(7);
    return byte;
}

fn bus.write(addr, byte) {
    clock(7);
    write_byte(addr, byte);
    clock(5);
}

Aside: I'm not fully sure these are the "correct" splits — on real silicon both reads and writes happen during the second half of the cycle, and the exact master clock isn't fixed at a 1-clock granularity. The 5/7 and 7/5 numbers here are what Mesen uses for the common case, which is a reasonable starting point but not the final word.

Chip Parallelism #2: Master-clock centric

This way is worse for reasons specified further down.

The outer loop advances the master clock one clock at a time, and on each clock a switch decides which chips are due to step:

let clock = 0;
while (true) {
    clock += 1;
    if (clock % 4  == 0) ppu.cycle();
    if (clock % 12 == 0) cpu.cycle();
    if (clock % 12 == 0) apu.cycle();
}

Now each chip moves exactly when its divider fires, no chip ever runs ahead of another. That makes mid-cycle effects natural to express — a PPU event that happens on clock N+3 of a CPU cycle just happens, without the CPU loop having to know about it.

This has two challenges to implement:

1. The CPU has to be written as a state machine

In the master-clock-centric loop, cpu.cycle() is called once per CPU cycle (every 12 clocks), but a full CPU instruction takes 2–7 cycles. So a single instruction can't run top-to- bottom in one call — it has to be sliced across as many calls as the instruction has cycles, with the CPU itself remembering where in the instruction it left off.

To see what this looks like in general: a straight-line function that does its work in one go looks like this:

fn cook_pizza() {
    knead_dough();
    add_toppings();
    bake();
}

If we can only do one step per call, the body has to become a switch on externalised state:

let stage = 0;

fn cook_pizza_step() {
    if      (stage == 0) knead_dough();
    else if (stage == 1) add_toppings();
    else if (stage == 2) bake();
    stage += 1;
}

Every NES instruction has to be written like this, with the cycle index as the stage. The CPU-centric model avoids it — instructions there stay as straight-line code.

This makes the code painful to write and slow at runtime: every instruction grows a switch, and the outer loop now runs 12× per CPU cycle instead of once.

2. Read/write timing inside a CPU cycle

Reads and writes land at different clocks within a CPU cycle, so the boundary check has to shift by an offset that depends on the access type of the next cycle. That means having a a per- instruction, per-cycle table of "is read or write?" so the loop has to know the right offset ahead of time:

while (true) {
    ...
    if ((clock + offset) % 12 == 0) cpu.cycle();
    ...
}

Again, annoying.


PPU

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.


Rendering Background

The background (also called the backdrop or playfield) is the scrolling world behind the sprites — the bricks, pipes and sky in Super Mario Bros. By the end of this chapter it's on screen, in color, and moving as the game scrolls.

Three steps:

  1. Static dump — the SMB title screen, grayscale. A warm-up that gets pixels up fast and gives us a known-good image to check against.
  2. The rendering pipeline — draw it the way the console does, one pixel per dot, fetching tiles as the beam sweeps. This is the bulk of the chapter, and it's what makes the screen scroll. Still grayscale.
  3. Color — feed the palette in.

Color comes last because it's a leaf you bolt on at the end — steps 1 and 2 stay in grayscale. We take for granted that the PPU's eight registers ($2000$2007) already read and write correctly, so VRAM holds the right nametable and the pattern tables hold the right tiles.

Step 1 — Static dump

A nametable is a 32×30 grid of tile indices; a pattern table turns each index into an 8×8 tile of 2-bit pixels. That's a whole screen, so draw it the dumb way: at the end of a frame, walk the 960 tile bytes, decode each tile, blit it into a 256×240 framebuffer.

for row in 0..30:
    for col in 0..32:
        let tile = nametable[row * 32 + col]
        let pixels = decode_tile(bg_pattern_table, tile)
        blit(framebuffer, pixels, col * 8, row * 8)
show(framebuffer)

This is the milestone-1 blit, with a nametable as the source instead of the raw pattern table. bg_pattern_table comes from PPUCTRL bit 4 ($0000 or $1000). Map the four pixel values to four grays — 0→white, 3→black:

3 3 3 3 3 3 3 3
1 1 1 1 1 1 1 3
2 2 2 2 2 2 2 3
2 1 2 1 2 1 1 3
2 2 1 2 1 1 1 3
2 1 2 1 2 1 1 3
2 2 1 2 1 1 1 3
2 1 2 1 2 1 1 3
One decoded background tile in four shades of gray

On the background, value 0 is not transparent the way it is for a sprite — it's the backdrop color, the sky. For now it's just another gray; in step 3 it becomes the universal background color.

Point this at Super Mario Bros. on its title screen and you should read "SUPER MARIO BROS." in gray — nametable 0 through pattern table 1. If it's garbled, stop: the problem is in the data or the decode, before any of the machinery below.

Step 2 — The rendering pipeline

The static dump can't scroll and isn't how the console works. The PPU draws one pixel per dot (one PPU clock; 341 per scanline) and never holds the whole nametable — it fetches a couple of bytes just ahead of the beam, clocks them out through shift registers, and walks the fetch address as it goes. That walk is the scroll: get it right and the screen moves for free.

Coarse and fine

Almost everything here is about naming a pixel, and the PPU splits that into two parts per axis: which tile, and which pixel inside the tile — the coarse and fine halves.

A pixel's position on each axis is coarse * 8 + fine. Scrolling by whole tiles moves the coarse part; scrolling by a few pixels moves the fine part.

One asymmetry that explains a lot of the later code: fine Y lives inside v, but fine X is its own register, x. Fine Y changes which bytes you fetch — a tile's pattern is stored one byte per row, so the read address has to include it. Fine X changes nothing about the fetch; it only picks which bit of an already-fetched byte comes out. So fine Y rides in the address, while fine X is held back and applied at the very end, as a pixel leaves the shift register.

The registers

The pipeline is short: VRAM → one latch per fetched byte → a shift register that clocks a bit out per dot → a pixel. The address registers v/t/x are the loopy registers from the PPU chapter; the latches and shift registers are new here.

v  15b  current VRAM address — read pointer and scroll
t  15b  staging copy; PPUSCROLL writes it, copied into v
x   3b  fine-X; picks which bit of a tile is output

  fetch latch                shift register (1 bit/dot out)
  ─────────────────────      ──────────────────────────────
  nt_latch   tile index      (none; only addresses pattern)
  bg_lo_latch  plane 0    →  bg_pattern_lo  16b ┐ 2 tiles
  bg_hi_latch  plane 1    →  bg_pattern_hi  16b ┘ in flight
  at_latch   attr byte    →  bg_attr_lo      8b ┐ current
                             bg_attr_hi      8b ┘ tile

The attribute byte covers a 4×4-tile block. The 2 bits for this tile's quadrant get parked in bg_attr_latch (2 bits) and fed into bg_attr_lo/hi one bit per dot — more on that at the boundary below.

The scanline

Each visible scanline (0–239) spends its 341 dots like this:

dots 0       idle
dots 1-256   draw the 32 visible tiles (8 dots each)
dots 257-320 sprite fetches — background idle
dots 321-336 prefetch the next line's first 2 tiles
dots 337-340 dummy fetches

Sprites are a later chapter, so ignore 257–320 for now. The pre-render line (261) runs the same fetches but draws nothing, priming the registers for line 0.

The 8-dot tile cycle

A tile is 8 pixels wide, so it gets 8 dots, and two things run in parallel the whole time. The shift registers clock pixels out — from a tile fetched earlier — while four two-dot reads pull a later tile's bytes in. At dot 8 the fetched tile drops into the registers and v steps one tile right.

The four reads. Every address comes from v, whose 15 bits are yyy NN YYYYY XXXXX — fine-Y / nametable / coarse-Y / coarse-X:

nt_addr = 0x2000 | (v & 0x0FFF)
at_addr = 0x23C0 | (v & 0x0C00)
                | ((v >> 4) & 0x38) | ((v >> 2) & 0x07)
bg_addr = (bg_pattern_table << 12) | (nt_latch << 4) | ((v >> 12) & 7)
dots 1-2:  nt_latch    = ppu_read(nt_addr)
dots 3-4:  at_latch    = ppu_read(at_addr)
dots 5-6:  bg_lo_latch = ppu_read(bg_addr)
dots 7-8:  bg_hi_latch = ppu_read(bg_addr + 8)

bg_addr is the low bitplane, bg_addr + 8 the high one — and both are built from nt_latch. That dependency fixes the order: you can't address the pattern until the nametable read says which tile it is, so NT comes first and the pattern bytes last. The attribute byte holds four quadrants' palettes; coarse X/Y pick this tile's 2-bit one:

quad_shift = ((v >> 4) & 4) | (v & 2)
palette    = (at_latch >> quad_shift) & 3

Where the bytes go. The pattern registers are 16 bits and hold two tiles: the high byte is the tile being drawn, the low byte the next one, already fetched. Fine-X selects the output bit, which is how a scroll part-way into a tile works:

fine-X picks the output bit:  bit = (reg >> (15 - x)) & 1

  15 .......... 8   7 .......... 0    ◄ shift left 1/dot
  └ current tile ┘  └─ next tile ─┘

That two-tile lead is why dots 321–336 of the previous line had to prefetch: at dot 1 of this line the registers are already primed.

The boundary (dot 8). The four latches reload the registers' low bytes, and the quadrant's palette bits go into bg_attr_latch:

bg_pattern_lo = (bg_pattern_lo & 0xFF00) | bg_lo_latch
bg_pattern_hi = (bg_pattern_hi & 0xFF00) | bg_hi_latch
bg_attr_latch = palette

Emit, every dot. On dots 1–256, read the top bit of each register for the pixel, then shift them all left — the attr registers taking a fresh bit from bg_attr_latch:

bit0 = (bg_pattern_lo >> (15 - x)) & 1
bit1 = (bg_pattern_hi >> (15 - x)) & 1
pal0 = (bg_attr_lo    >> (7 - x)) & 1
pal1 = (bg_attr_hi    >> (7 - x)) & 1
value   = (bit1 << 1) | bit0
palette = (pal1 << 1) | pal0
framebuffer[scanline][dot - 1] = gray[value]

bg_pattern_lo <<= 1
bg_pattern_hi <<= 1
bg_attr_lo = (bg_attr_lo << 1) | (bg_attr_latch & 1)
bg_attr_hi = (bg_attr_hi << 1) | (bg_attr_latch >> 1)

Grayscale ignores palette for now — step 3 reads it instead of throwing it away. Here's a row coming out, real SMB pixels as fat blocks, the tile boundaries every 8 dots:

A row of background pixels with tile boundaries marked every eight pixels

And the same row with x = 3 — the bit selection starts three pixels into the tile, so the whole row slides left, boundaries and all:

The same row shifted left by three pixels, the tile boundaries shifted with it

Scrolling — walking v

Nothing above moved the camera; v only has to advance correctly, and because it's both the read pointer and the scroll position, that advance is the scroll. Four updates, at fixed points in the line:

Every 8 dots — increment X. Step one tile right; at coarse X 31, wrap to 0 and flip the horizontal nametable bit so the walk continues into the next screen:

if (v & 0x001F) == 31:     # coarse X at the last column
    v &= ~0x001F           # wrap it to 0
    v ^= 0x0400            # flip horizontal nametable
else:
    v += 1

At dot 256 — increment Y. Step one pixel row down: bump fine Y, carry into coarse Y, flip the vertical nametable at row 29.

At dot 257, and again on the pre-render line — reload from t. By now v has walked off to the right and down, so it's restored from t, the staging copy PPUSCROLL writes — the horizontal bits every line, the vertical bits once per frame:

dot 257:       v = (v & ~0x041F) | (t & 0x041F)   # X
dots 280-304:  v = (v & ~0x7BE0) | (t & 0x7BE0)   # Y, pre-render only

These reloads are the only path from the scroll the game wrote into v, so this is where scrolling actually happens. Pin v = t = $2000, x = 0 and you get step 1's title screen back exactly; let PPUSCROLL move them and SMB's overworld scrolls as Mario walks, straddling two nametables (SMB uses vertical mirroring). Still grayscale — the only thing that can be wrong here is the address math.

Step 3 — Color

Every pixel already arrives with a 4-bit index: palette (2 bits) and value (2 bits). Turn it into a palette-memory address:

let addr = 0x3F00 | (palette << 2) | value

$3F00$3F1F is palette RAM (the palette table): eight 4-color palettes, the low half for the background. Value 0 is the catch — every palette's entry 0 mirrors $3F00, the universal backdrop, the one color behind everything (the sky). That's why background value 0 wasn't transparent in step 1:

if value == 0:
    addr = 0x3F00
let nes_color = palette_ram[addr & 0x1F] & 0x3F

That's a 6-bit number, 063: an index into the master palette (the system palette), the 2C02's fixed 64-color RGB table. One more lookup and the grayscale write from step 2 becomes a real pixel:

framebuffer[scanline][dot - 1] = MASTER_PALETTE[nes_color]

The result is blocky color regions: the palette comes from the attribute table, where every 2×2 tiles share one entry, so color is assigned in 16×16-pixel patches. Here's the step-1 tile in gray, then the same tile once its index runs through the two lookups:

The tile in grayscale, the step 1 output The same tile in color after the palette lookup

What's left

The background is on screen, colored and scrolling. The remaining background details don't change the picture much:

The next thing that does change the picture is sprites, which ride on top of all this — and where the mid-frame scroll split (a fixed status bar over a moving level) finally gets driven, by sprite-0 hit.