Skip to content

A basic NES emulator written in Rust with an emphasis on debugging and game development.

License

Notifications You must be signed in to change notification settings

ndouglas/greenstone

Repository files navigation

Greenstone

An NES emulator written in Rust, with a focus on cycle accuracy and understanding the hardware at a deep level.

Why an NES Emulator?

The NES is a fascinating machine to emulate. It's simple enough to be tractable (the 6502 has only 56 instructions, though with addressing modes that expands to 151 official opcodes plus dozens of "illegal" ones), but complex enough to be interesting. The real challenge isn't the CPU—it's the PPU.

The Picture Processing Unit is where things get weird. It's not a framebuffer-based system. Instead, the PPU renders pixels in real-time, racing the electron beam across the screen. Games exploit this timing in creative ways: they change scroll positions mid-scanline, swap pattern tables during vblank, and trigger sprite-0 hits to split the screen. Getting these timing-dependent tricks right requires understanding not just what the hardware does, but when it does it.

This project is my attempt to understand that timing at a deep level.

Current State

What works:

  • Complete 6502 CPU implementation with all official opcodes
  • All "illegal" (undocumented) opcodes that games actually use
  • Cycle-accurate instruction timing
  • Cartridge loading with iNES format parsing
  • Mapper 0 (NROM), Mapper 1 (MMC1), and Mapper 2 (UxROM) support
  • PPU rendering with background tiles and sprites
  • Horizontal and vertical nametable mirroring
  • Cycle-accurate VBlank timing (passes blargg's ppu_vbl_nmi tests 01-04, 06)
  • NMI generation with proper edge cases (suppression race condition, mid-vblank enable)
  • Sprite-0 hit detection
  • Controller input
  • Optional WebSocket debug server for external tool integration

What's in progress:

  • Fine-tuning NMI timing (test 05 is off by 1 instruction in some cases)
  • Sprite overflow flag with hardware bug emulation

What's planned:

  • Additional mappers (MMC3, etc.)
  • APU (audio)
  • More blargg test compatibility

Screenshots

Super Mario Bros Tetris Castlevania
Super Mario Bros Tetris Castlevania

Architecture

The emulator is structured around trait-based abstractions that mirror the NES hardware:

┌─────────────────────────────────────────────────────────┐
│                         Bus                             │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌───────────┐   │
│  │   CPU   │  │   PPU   │  │   RAM   │  │ Cartridge │   │
│  │  6502   │  │  2C02   │  │   2KB   │  │  + Mapper │   │
│  └─────────┘  └─────────┘  └─────────┘  └───────────┘   │
└─────────────────────────────────────────────────────────┘

Key traits:

  • Addressable — Read/write operations at 16-bit addresses
  • Busable — Full bus interface combining Addressable + Interruptible
  • Interruptible — NMI and IRQ signal handling
  • Mappable — Cartridge mapper interface for bank switching

The CPU ticks the bus, which ticks the PPU at a 3:1 ratio (the PPU runs at 5.37 MHz vs the CPU's 1.79 MHz). This timing relationship is critical for accurate emulation.

What I've Learned

Building this has taught me things about hardware design that I wouldn't have learned any other way:

The PPU is a state machine, not a graphics card. Modern GPUs accept commands and render frames. The NES PPU is a circuit that outputs one pixel per cycle, consulting memory as it goes. It doesn't "know" what the final frame will look like—it just follows its state machine, and games manipulate that state to create effects.

Memory-mapped I/O is elegant but tricky. Reading $2002 (PPU status) has side effects—it clears the vblank flag and resets the address latch. Writing $2006 twice sets a 14-bit address. These aren't just memory locations; they're hardware interfaces with behavior.

Cycle accuracy matters more than I expected. Many games work fine with approximate timing. But the moment you try to run something that uses sprite-0 hits for a status bar, or changes scroll position mid-frame, you discover that "close enough" isn't.

The 6502's illegal opcodes are real instructions. They're not random behavior—they're the result of how the CPU's microcode combines operations. LAX loads both A and X. DCP decrements memory then compares. Games use these, so emulators must support them.

Running

# Build and run with a ROM
cargo run --release -- -f path/to/rom.nes

# Run with debug server (WebSocket on port 44553)
cargo run --release -- -f path/to/rom.nes --serve

# Run tests
cargo test

# Run with trace logging
RUST_LOG=trace cargo test test_name

Controls

Controller 1 is mapped to the keyboard:

NES Button Key
D-Pad Arrow keys
A J
B K
Select U
Start I

Debug Server

The --serve flag starts a WebSocket server on port 44553. This is intended for external debugging tools—you can connect to inspect emulator state, set breakpoints, or build visual debuggers without modifying the emulator itself. The server runs alongside the emulator and doesn't affect normal operation.

Testing

Each CPU instruction has its own test file under src/nes/cpu/instructions/. The test macro validates:

  • Register state changes
  • Status flag behavior (respecting the instruction's flag mask)
  • Memory modifications
  • Cycle counts
test_instruction!("ADC", Immediate, [0x03]{a: 0x02} => []{a: 0x05});
//                 ^      ^          ^     ^           ^   ^
//                 |      |          |     |           |   expected state
//                 |      |          |     initial     expected memory
//                 |      |          operand bytes
//                 |      addressing mode
//                 mnemonic

Acknowledgements

This project draws heavily from the NES development community:

About

A basic NES emulator written in Rust with an emphasis on debugging and game development.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages