Post

Building a NES Emulator from Scratch: The Book (Crystal)

I built a NES emulator in Crystal. Mario went from 0.5 FPS to 60 FPS. Then I turned the whole journey into a book. Here's what I learned.

In my last post I mentioned I’d spent a few months writing a book about building a NES emulator from scratch. Well, it’s done. And I want to tell you about it, because I think the journey from “I wonder how emulators work” to “I wrote a 280-page book about it” is kind of ridiculous and worth sharing.

How we got here

It started, as most bad decisions do, at 2 AM. I was playing Mario Bros in a browser emulator, died in world 2-3, and instead of going to sleep like a normal person, I started wondering how the emulator worked. A few weeks later I had a working emulator in Crystal running at 60 FPS. A few months after that, I had a book.

The thing is, while building the emulator I kept thinking: “I wish someone had explained this to me step by step.” The NES Wiki is incredible but dense. YouTube tutorials assume you already know C and have opinions about memory allocation strategies. I wanted something that started from zero and built up piece by piece, with code first and theory after.

So I wrote that thing.

fine I'll do it myself

What’s in the book

The book follows the order I actually built the emulator. You start with a CPU that does nothing, teach it to load a number into a register, then add, then jump, and so on until you have all 151 instructions of the 6502 implemented. Then you build the PPU, get pixels on screen, and eventually you’re playing Mario.

Here’s the chapter breakdown:

  • Chapters 1-2: NES architecture overview + Crystal setup
  • Chapter 3 (7 sub-chapters): The entire 6502 CPU, all 151 opcodes
  • Chapter 4: Cartridge parsing, iNES format, Mapper 0
  • Chapter 5 (6 sub-chapters): PPU, SDL2 GUI, background rendering, sprites, scroll
  • Chapter 6: Plugging in real games and watching them run
  • Chapter 7: APU, generating audio with square, triangle and noise waves
  • Appendix: Mapper 1 (MMC1) for games like Zelda and Mega Man 2

Everything is written in Crystal, which reads almost exactly like Ruby. No C, no emulation libraries.

Some actual code

The best way to explain the book’s approach is to show some code from it. Here’s the CPU’s main loop. It has registers, a program counter, and a step method that fetches the next opcode and executes it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# src/nes/cpu.cr

getter a      : UInt8   # Accumulator
getter x      : UInt8   # X register
getter y      : UInt8   # Y register
getter sp     : UInt8   # Stack Pointer
property pc   : UInt16  # Program Counter
getter status : UInt8   # Flags (Zero, Negative, Carry, etc.)

def step
  opcode = fetch_byte

  case opcode
  when CODE_LDA_IMMEDIATE   then op_lda_immediate
  when CODE_LDA_ZERO_PAGE   then op_lda_zero_page
  when CODE_LDA_ABSOLUTE    then op_lda_absolute
  when CODE_LDA_ABSOLUTE_X  then op_lda_absolute_x
  # ... STA, LDX, LDY, ADC, SBC, JMP, branches ...
  when CODE_INX             then op_inx
  when CODE_NOP             then op_nop
  else raise UnknownOpcode.new(opcode)
  end

  CYCLES[opcode]
end

Fetch a byte, match it against 151 opcodes, execute the right method, return how many cycles it took. The case statement looks intimidating but each instruction is just a few lines.

Let’s zoom into one. When the CPU reads 0xA9 from memory, it runs LDA (Load Accumulator) in immediate mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
# src/nes/cpu/instructions/lda.cr

def lda(value)
  @a = value

  set_z_flag(@a)
  set_n_flag(@a)
end

def op_lda_immediate
  value = fetch_byte
  lda(value)
end

Read a byte, put it in register A, update the flags. The lda method is reusable across all 8 addressing modes, each one just resolves the address differently:

1
2
3
4
5
6
7
8
9
10
11
12
13
def op_lda_zero_page
  address = address_zero_page
  value = read_byte(address)
  lda(value)
end

def op_lda_absolute
  address = address_absolute
  value = read_byte(address)
  lda(value)
end

# ... and so on for all 8 modes

Once you implement one instruction family, the rest follow the same structure. The book shows you a few in detail, you implement 10-15 yourself to internalize how the CPU works, and then you grab the rest from the repo. No one needs to hand-type 151 opcodes.

The PPU

The CPU is cool but the PPU is where I spent the most time. The NES draws an entire screen with 2KB of RAM. Two kilobytes. Your average email is bigger than that.

The PPU (Picture Processing Unit) is a separate chip that runs 3 times faster than the CPU and has its own memory. It draws the screen scanline by scanline, 256x240 pixels, 60 times per second. The book walks you through it layer by layer: first a black screen, then the background, then sprites, then scroll. Each chapter adds one thing and you can see the progress on screen.

When Mario’s title screen showed up for the first time, I just sat there staring at it for a good minute. And then I pressed Start and nothing happened because of a missing feature called sprite 0 hit (in the book I’ll tell you all about it). Classic.

why

The emulation loop

My favorite part of the whole emulator is how simple the core loop ends up being:

1
2
3
4
5
6
def step
  cycles = @cpu.step
  (cycles * 3).times { @ppu.step }
  @apu.step(cycles)
  cycles
end

The CPU executes one instruction and returns how many cycles it took. The PPU runs 3 times as fast (that’s the real hardware ratio). The APU keeps up. That’s it. Everything else is implementing the details behind each .step.

Play it in the browser

I compiled a Rust rewrite of the emulator to WebAssembly so there’s a playable version:

👉 emulator.matiassalles99.codes

The book

The book is on Leanpub in English and Spanish:

There’s a free sample that covers the intro, NES architecture, Crystal setup, and the first coding chapter where you build the CPU skeleton and implement your first two instructions.

If you know Ruby, Python, or any similar language, you can follow along. The book doesn’t assume any emulation or hardware knowledge.

What’s next

I’m going to keep building stuff in Crystal and writing about it here. If you build the emulator or read the book, let me know.

Now if you’ll excuse me, I need to go beat world 2-3.

mario

This post is licensed under CC BY 4.0 by the author.