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.
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.
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:
- 🇬🇧 English: Building Your First Emulator
- 🇪🇸 Español: Construí tu Primer Emulador
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.



