cge_nes
A cycle-accurate NES emulator core written in Rust (2021 edition). Front-end agnostic: the library handles CPU, PPU, cartridge, and input emulation; you bring your own rendering, windowing, and host input.
Features:
- Cycle-accurate 6502 CPU with multi-cycle instruction execution
- Bit-accurate PPU with precise sprite rendering, palette handling, and all four mirroring modes
- iNES ROM loading and cartridge mapper support (mappers 0, 1, 2, 3, 4)
- Composable memory devices (CPU bus and PPU bus) built from
devices6502 - Decoupled input: you implement
HostInput, the emulator polls each frame no_std-friendly core- Single dependency (
cge_nes) gets you the whole stack
Roadmap: APU (audio) emulation is not yet implemented in this release but is planned for a future one.
Quick facts:
- 256×240 pixel frame buffer, one already-resolved NES palette color per pixel
- 262 scanlines per frame (~29,780 CPU cycles, ~60 Hz NTSC)
- CPU and PPU synchronized every cycle; PPU runs at 3× the CPU clock
Contents
- Getting started
- Architecture
- Module layout
- Public API
- Features
- Build
- Testing
- Quick start
- Front-end integration
- Module reference
- Development notes
- License
Getting started
Prerequisites:
- Rust 1.75+ (Rust 2021 edition)
- Windows / Linux / macOS
Add to your project:
[]
= "0.1"
Build the library, examples, and binaries:
Architecture
High-Level Design
┌────────────────────────────────────────────────────┐
│ Your Front-End │
│ (Rendering, Input, Frame Loop, UI) │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ cge_nes │
│ ┌──────────────────────────────────────────────┐ │
│ │ nes::NesConsole │ │
│ │ ├─ cpu (cpu6502) + cpu_bus (devices6502) │ │
│ │ ├─ ppu::Ppu + ppu_bus │ │
│ │ ├─ cartridge::Cartridge trait │ │
│ │ ├─ input::HostInput trait │ │
│ │ └─ nes::dma (OAM DMA) │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
Key Concepts
- CPU & Memory: The
Cpufromcpu6502executes against a memory map built fromdevices6502traits. CPU RAM, PPU registers, and cartridge ROM/RAM are all addressable in one 16-bit address space. - PPU & Video Output: The
Ppurenders pixels scanline-by-scanline, reading CHR ROM, name tables, attribute tables, and OAM. Frame buffer is exposed as&[Color](one NES palette color per pixel). - Cartridge Loading:
rom_loaderparses iNES ROM files and instantiates the matching mapper. Mappers handle bank switching, mirroring, and PRG RAM. - Input:
input::HostInputis the trait your front-end implements. The emulator polls it once per frame per controller port viarun_frame.
Execution Model
The NES runs in a frame loop:
- Frame setup: Reset scanline counter, clear frame buffer.
- Scanline loop (262 scanlines per frame):
- CPU cycles: Execute multiple 6502 instructions per scanline (~114 CPU cycles).
- PPU rendering: PPU advances each cycle in lockstep with CPU; pixels are generated from pattern / name / attribute / OAM lookups.
- Sprites & priority: OAM sprites evaluated and rendered with correct priority each visible scanline.
- Frame complete: Frame buffer is ready for your front-end to render.
- Input polling (optional, your front-end decides): Sample host input, update controller state.
- Repeat.
Module layout
cge_nes/
├── README.md # This file — single source of docs
├── LICENSE # MIT (also LICENSE-MIT, LICENSE-APACHE)
├── docs/ # Hardware reference (mappers, etc.)
├── examples/
│ └── run_frame.rs # Canonical front-end bootstrap example
├── src/
│ ├── lib.rs # Public surface + module declarations + re-exports
│ ├── bin/
│ │ └── ines_header_inspector.rs # Debug tool: dumps iNES headers from test_assets/
│ ├── cartridge/ # Cartridge trait (shared abstraction)
│ ├── input/ # HostInput trait + gamepad state
│ ├── ppu/ # PPU emulation
│ ├── nes/ # System integration (CPU + PPU + cartridge + input)
│ └── rom_loader/ # iNES parser + mapper dispatch
└── test_assets/ # Real .nes ROMs (gitignored, for manual testing)
External dependencies (from crates.io):
cpu6502— 6502 CPU emulatordevices6502— composable memory devicesbitflags,arrayvec,ringbuffer— small utility crates
Public API
The crate root re-exports every commonly-needed type. Most front-ends only need a handful of these:
use ;
Lower-level access (PPU internals, mapper implementations, etc.) is available through the module hierarchy:
use ;
use ;
use NesConsole; // same as the re-exported NesConsole
Features
Per-feature flags:
| Feature | Purpose |
|---|---|
instr_log |
CPU instruction logging (writes every instruction to nes_cpu_log.txt). Enables cpu6502/logging under the hood. |
oam_array_raw |
Alternative OAM memory layout (profiling / optimization experiments). |
show_name_table_change |
Debug name / attribute table access during rendering. |
mapper_debug_log |
Mapper bank-switching traces. |
Build
# Default build (library + examples + binaries)
# With specific features
# Release
Testing
# Run all library + integration + binary tests
# Per-target
# Show backtrace for a failing test
RUST_BACKTRACE=1
Quick start
The canonical bootstrap lives as a runnable example at
examples/run_frame.rs:
It loads the ROM, runs one frame, and prints the buffer dimensions plus the count of non-background pixels. The source is the recommended starting point for a front-end. The skeleton, condensed:
use ;
;
let rom_data = read?;
let cartridge = load_rom?;
let mut console = new;
console.insert_cartridge;
console.switch_on;
let mut host = NullHostInput;
console.run_frame;
let frame: & = console.screen_colors;
// render `frame` to your window of choice.
Replace NullHostInput with an adapter that polls your keyboard / gamepad
each frame.
Front-end integration
This library is front-end agnostic. You supply:
- Rendering: convert
&[Color](256×240 already-resolved NES palette colors) to your target pixel format and display. - Input handling: implement
HostInputto expose host keyboard / gamepad state asGamepadState. - Frame timing: drive
run_frameat your own cadence (~60 Hz for authentic speed, or unlimited for testing). - Audio (optional): APU emulation is not yet implemented in this crate (planned for a future release); if you need audio now, expose the APU register reads/writes from your front-end and render the samples yourself.
Suggested front-end stacks:
- SDL2 — simple cross-platform graphics and input
- wgpu — modern GPU-accelerated rendering
- Bevy — full-featured game engine with ECS
- druid / iced — GUI framework for emulator settings / UI
- rodio — audio playback (once APU support lands in this crate, or if you implement it yourself)
Module reference
cartridge — Cartridge trait
The contract every NES mapper must satisfy. The system bus dispatches reads and writes here; front-end code does not call these directly.
ChrRomContentStatus (returned by writes) lets the PPU decide whether its
caches need invalidating.
input — Host input
InputRegisters (also re-exported) drives the serial shift-register protocol
at $4016 / $4017. Most front-ends don't need to touch it directly —
NesConsole::run_frame handles strobe and shift transparently.
ppu — Picture Processing Unit
Rendering pipeline (per visible scanline, 0–239):
- Name table lookup: For each tile (32 × 8 = 256 pixels wide), read tile ID from the name table.
- Attribute table lookup: Read palette index (0–3) from the attribute table.
- Pattern table lookup: Fetch the 8×8 tile bitmap from CHR ROM using the tile ID.
- Sprite evaluation: For each scanline, evaluate OAM to find up to 8 sprites per scanline.
- Pixel composition: For each pixel, composite background and sprite layers with priority.
Memory organization:
- PPU registers (
$2000–$2007):$2000PPUCTRL — NMI enable, sprite size, name-table base$2001PPUMASK — rendering enable, emphasis bits$2002PPUSTATUS — VBLANK, sprite overflow, sprite-0 hit$2003OAMADDR /$2004OAMDATA — OAM access$2005PPUSCROLL — background scroll X/Y$2006PPUADDR /$2007PPUDATA — VRAM access with read buffer
- VRAM (
$0000–$3FFF):$0000–$1FFF— CHR ROM (graphics from cartridge)$2000–$2FFF— name tables with mirroring$3000–$3EFF— mirror of name tables$3F00–$3FFF— palette RAM (16 entries × 4 backgrounds + 4 sprites)
- OAM — 256 bytes, 64 sprites × 4 bytes (Y, tile ID, attributes, X)
Mirroring modes:
- Horizontal — name tables 0 & 1 are horizontally mirrored; 2 & 3 too
- Vertical — name tables 0 & 2 are vertically mirrored; 1 & 3 too
- One-screen — single name table repeated (single-screen boards)
- Four-screen — cartridge provides all four name tables (e.g. MMC3 with no mirroring bit set)
API:
nes — System integration
NesConsole wires together CPU, PPU, cartridge, and input.
System components:
- CPU bus — 16-bit address space:
$0000–$07FF— CPU internal RAM (2 KB, mirrored 4×)$2000–$2007— PPU registers$4000–$401F— APU and I/O registers (including controller input)$8000–$FFFF— cartridge ROM/RAM
- PPU bus — video memory (see
ppumodule). - OAM (Object Attribute Memory) — 256 bytes of sprite data
- Controllers — two ports (Player 1 / Player 2) polled through
input::HostInput
Execution model:
NesConsole::run_frame(&mut host_input) executes one complete frame:
- Scanlines 0–239: visible rendering area
- Scanline 240: post-render (VBLANK starts)
- Scanlines 241–260: vertical blank period (no PPU rendering)
- Scanline 261: pre-render (sprite evaluation, flag clearing)
Internally the CPU executes ~114 cycles per scanline; the PPU runs at 3× CPU clock so each CPU cycle produces three PPU cycles.
API:
Internal module layout:
nes::system—NesConsoletype and frame executionnes::ppu_cart_memory— PPU address space (CHR ROM, VRAM, palette)nes::dma— OAM DMA controller for sprite data transfers
rom_loader — iNES parser and mapper dispatch
Given a 16-byte iNES header plus the trailing PRG / CHR bytes, returns a
Box<dyn Cartridge> ready for NesConsole::insert_cartridge.
iNES file format:
Offset Size Description
------ ---- -----------
0x00 4 Magic: "NES\x1A"
0x04 1 PRG ROM size in 16 KB units
0x05 1 CHR ROM size in 8 KB units (0 means CHR RAM)
0x06 1 Flags 6: mapper low, mirroring, PRG RAM, trainer, four-screen
0x07 1 Flags 7: mapper high, iNES 2.0 marker
0x08+ var Optional 512-byte trainer (if flag 6 bit 2 is set)
N+ var PRG ROM data
N+M var CHR ROM data
API:
pub type LoadRomResult = ;
;
Supported mappers:
| ID | Name | Features | Status |
|---|---|---|---|
| 0 | NROM | No bank switching; 16 KB or 32 KB PRG; 8 KB CHR ROM or CHR RAM | ✅ Implemented |
| 1 | MMC1 | PRG/CHR bank switching, mirroring, PRG RAM protect | ✅ Implemented |
| 2 | UxROM | 16 KB switchable PRG + fixed last bank; CHR fixed | ✅ Implemented |
| 3 | CNROM | 32 KB fixed PRG; 8 KB switchable CHR ROM | ✅ Implemented |
| 4 | MMC3 | PRG/CHR bank switching, scanline IRQ counter | ✅ Implemented |
| Others | — | — | ❌ Not implemented (returns RomError::UnsupportedMapper(mapper_id)) |
NROM, UxROM, and CNROM share rom_loader::ines::mappers::basic_mapper. Bus
conflicts that real UxROM / CNROM boards exhibit on writes to
$8000–$FFFF are intentionally not emulated.
Adding new mappers: implement Cartridge, create
src/rom_loader/ines/mappers/mapperNNN.rs with pub fn load(&HeaderData, &mut impl Read) -> LoadRomResult,
add a match arm in rom_loader::load_rom, and add unit tests.
Known issues:
- Trainer block not skipped. When header[6] bit 2 is set, the 512-byte trainer area is read as if it were PRG ROM.
- NES 2.0 largely ignored. The
ines2flag is not decoded; extended fields in header[8..15] are dropped. NES 2.0 ROMs parse as iNES 1.0. - No board-quirk handling on mappers 1 and 4 beyond the basics.
ines_header_inspector binary
A debug tool that scans test_assets/ for .nes files and writes
header_info.txt summarising each ROM's header:
Run from the repo root so it can find the test_assets/ directory.
Development notes
-
Architecture philosophy:
- Each top-level module (
cartridge,input,ppu,nes,rom_loader) has a single responsibility and a narrow public surface - Memory built from trait-based device composition via
devices6502, with no allocations in the hot path - Testable: per-module tests verify behavior in isolation
- Performance-conscious: cycle accuracy without overhead; bit-accurate pixel rendering
- Each top-level module (
-
When modifying the PPU:
- PPU rendering is bit-accurate; small changes to pixel math can break test ROM behavior
- Add focused unit tests for sprite priority, palette selection, and flipping
- Test against ROM test suites (e.g.
nestest) if available - VRAM read buffering is a hardware quirk: the first read after an address write returns the buffered value, not the new value
-
When modifying cartridge / mapper logic:
- Mappers handle bank switching and address translation; errors affect every CPU / PPU memory access
- Add mapper-specific tests for bank switching sequences and ROM paging
- Use the
mapper_debug_logfeature to trace bank-switching
-
Performance tuning (from
Cargo.toml):[profile.dev.package."*"] opt-level = 3— fast dependency builds[profile.dev] opt-level = 1— reasonable debug iteration[profile.release] lto = "fat"— full link-time optimization- Profile CPU-heavy paths with
perforflamegraph
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.