# cge_nes
[](https://crates.io/crates/cge_nes)
[](https://docs.rs/cge_nes)
[](https://github.com/CasiussDev/cge_nes/blob/main/LICENSE-MIT)
[](https://github.com/CasiussDev/cge_nes/blob/main/LICENSE-APACHE)
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](#getting-started)
- [Architecture](#architecture)
- [Module layout](#module-layout)
- [Public API](#public-api)
- [Features](#features)
- [Build](#build)
- [Testing](#testing)
- [Quick start](#quick-start)
- [Front-end integration](#front-end-integration)
- [Module reference](#module-reference)
- [Development notes](#development-notes)
- [License](#license)
## Getting started
**Prerequisites:**
- Rust 1.75+ (Rust 2021 edition)
- Windows / Linux / macOS
**Add to your project:**
```toml
[dependencies]
cge_nes = "0.1"
```
**Build the library, examples, and binaries:**
```bash
cargo build
cargo build --examples
cargo build --bins
```
## 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 `Cpu` from `cpu6502` executes against a memory map
built from `devices6502` traits. CPU RAM, PPU registers, and cartridge
ROM/RAM are all addressable in one 16-bit address space.
- **PPU & Video Output**: The `Ppu` renders 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_loader` parses iNES ROM files and instantiates
the matching mapper. Mappers handle bank switching, mirroring, and PRG RAM.
- **Input**: `input::HostInput` is the trait your front-end implements. The
emulator polls it once per frame per controller port via `run_frame`.
### Execution Model
The NES runs in a frame loop:
1. **Frame setup**: Reset scanline counter, clear frame buffer.
2. **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.
3. **Frame complete**: Frame buffer is ready for your front-end to render.
4. **Input polling** (optional, your front-end decides): Sample host input,
update controller state.
5. **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 emulator
- `devices6502` — composable memory devices
- `bitflags`, `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:
```rust
use cge_nes::{
// System integration
NesConsole,
// ROM loading
load_rom, LoadRomResult, RomError,
// Cartridge contract (implement for custom mappers)
Cartridge, ChrRomContentStatus,
// Input
ConnectedSocket, GamepadButtonsPressedFlags, GamepadState,
HostInput, InputDeviceState,
// Video
Color,
};
```
Lower-level access (PPU internals, mapper implementations, etc.) is available
through the module hierarchy:
```rust
use cge_nes::ppu::{Ppu, PpuCartMemorySpace, FrameEvent, Register};
use cge_nes::rom_loader::ines::{HeaderData, Mirroring};
use cge_nes::nes::system::NesConsole; // same as the re-exported NesConsole
```
## Features
Per-feature flags:
| `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. |
```bash
cargo build --features instr_log
cargo build --features oam_array_raw,show_name_table_change
cargo build --features mapper_debug_log
cargo build --all-features # enables everything; some pre-existing PP U debug paths require a manual fix-up
```
## Build
```bash
# Default build (library + examples + binaries)
cargo build
# With specific features
cargo build --features instr_log
cargo build --features oam_array_raw,show_name_table_change
cargo build --features mapper_debug_log
# Release
cargo build --release
```
## Testing
```bash
# Run all library + integration + binary tests
cargo test
# Per-target
cargo test --lib
cargo test --tests
cargo test --bins
# Show backtrace for a failing test
RUST_BACKTRACE=1 cargo test render_scanline
```
## Quick start
The canonical bootstrap lives as a runnable example at
[`examples/run_frame.rs`](examples/run_frame.rs):
```bash
cargo run --example run_frame -- path/to/game.nes
```
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:
```rust
use cge_nes::{
ConnectedSocket, HostInput, InputDeviceState, NesConsole, load_rom,
};
struct NullHostInput;
impl HostInput for NullHostInput {
fn input_device_state(&mut self, _player: ConnectedSocket) -> InputDeviceState {
InputDeviceState::Disconnected
}
}
let rom_data = std::fs::read("game.nes")?;
let cartridge = load_rom(&mut rom_data.as_slice())?;
let mut console = NesConsole::new();
console.insert_cartridge(cartridge);
console.switch_on();
let mut host = NullHostInput;
console.run_frame(&mut host);
let frame: &[cge_nes::Color] = 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:
1. **Rendering**: convert `&[Color]` (256×240 already-resolved NES palette
colors) to your target pixel format and display.
2. **Input handling**: implement `HostInput` to expose host keyboard /
gamepad state as `GamepadState`.
3. **Frame timing**: drive `run_frame` at your own cadence (~60 Hz for
authentic speed, or unlimited for testing).
4. **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.
```rust
pub trait Cartridge {
fn read_cpu_mapped(&self, addr: u16) -> u8;
fn write_cpu_mapped(&mut self, data: u8, addr: u16) -> ChrRomContentStatus;
fn read_ppu_mapped(&mut self, addr: u16) -> u8;
fn write_ppu_mapped(&mut self, data: u8, addr: u16) -> ChrRomContentStatus;
fn irq_pin(&self) -> bool { false } // override for MMC3
fn requires_cycle_accurate_sprites(&self) -> bool { false } // override for MMC3
fn notify_vram_addr_change(&mut self, _old_addr: u16, _new_addr: u16) {} // A12-clocked counters
}
```
`ChrRomContentStatus` (returned by writes) lets the PPU decide whether its
caches need invalidating.
### `input` — Host input
```rust
pub trait HostInput {
fn input_device_state(&mut self, player: ConnectedSocket) -> InputDeviceState;
}
pub enum InputDeviceState {
Gamepad(GamepadState),
Disconnected, // default
}
pub struct GamepadState {
pub buttons_pressed: GamepadButtonsPressedFlags, // bitflags: A, B, SELECT, START, UP, DOWN, LEFT, RIGHT
}
pub enum ConnectedSocket { Player1 = 0, Player2 = 1 }
```
`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):
1. **Name table lookup**: For each tile (32 × 8 = 256 pixels wide), read
tile ID from the name table.
2. **Attribute table lookup**: Read palette index (0–3) from the attribute
table.
3. **Pattern table lookup**: Fetch the 8×8 tile bitmap from CHR ROM using
the tile ID.
4. **Sprite evaluation**: For each scanline, evaluate OAM to find up to 8
sprites per scanline.
5. **Pixel composition**: For each pixel, composite background and sprite
layers with priority.
**Memory organization**:
- **PPU registers** (`$2000–$2007`):
- `$2000` PPUCTRL — NMI enable, sprite size, name-table base
- `$2001` PPUMASK — rendering enable, emphasis bits
- `$2002` PPUSTATUS — VBLANK, sprite overflow, sprite-0 hit
- `$2003` OAMADDR / `$2004` OAMDATA — OAM access
- `$2005` PPUSCROLL — background scroll X/Y
- `$2006` PPUADDR / `$2007` PPUDATA — 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**:
```rust
impl Ppu {
pub fn new() -> Self;
pub fn reset(&mut self);
pub fn screen_colors(&self) -> &[Color]; // 256×240 already-resolved colors
/// Advance the PPU by one cycle.
pub fn run_cycle(&mut self, cart: &mut impl PpuCartMemorySpace) -> FrameEvent;
pub fn nmi_signal(&self) -> bool;
pub fn write_ppu_register(&mut self, value: u8, register: Register, cart: &mut impl PpuCartMemorySpace);
pub fn read_ppu_register(&mut self, register: Register, cart: &mut impl PpuCartMemorySpace) -> u8;
pub fn write_ppu_register_by_addr(&mut self, addr: u16, value: u8, cart: &mut impl PpuCartMemorySpace);
pub fn read_ppu_register_by_addr(&mut self, addr: u16, cart: &mut impl PpuCartMemorySpace) -> u8;
}
pub enum FrameEvent { None, EndOfScanline, ReadyToPresent, EndOfFrame }
pub enum Register { PpuControl, PpuMask, PpuStatus, OamAddr, OamData, PpuScroll, PpuAddr, Data }
```
### `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 `ppu` module).
- **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**:
```rust
impl NesConsole {
pub fn new() -> Self;
pub fn insert_cartridge(&mut self, cart: Box<dyn Cartridge>); // panics if already inserted
pub fn switch_on(&mut self); // power on (resets CPU + PPU)
pub fn switch_off(&mut self);
pub fn reset(&mut self); // CPU + PPU + cycle counter reset
pub fn remove_cartridge(&mut self); // panics if currently on
pub fn cartridge_connected(&self) -> bool;
pub fn on(&self) -> bool;
pub fn screen_colors(&self) -> &[Color];
/// Low-level: one CPU cycle + three PPU cycles.
pub fn run_console_cycle(&mut self, cart: &mut dyn Cartridge, host: &mut impl HostInput) -> FrameEvent;
/// Run until the PPU indicates the next frame is ready to present.
pub fn run_until_present(&mut self, host: &mut impl HostInput);
/// Run one complete frame (262 scanlines).
pub fn run_frame(&mut self, host: &mut impl HostInput);
}
```
**Internal module layout**:
- `nes::system` — `NesConsole` type and frame execution
- `nes::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**:
```rust
pub type LoadRomResult = Result<Box<dyn Cartridge>, RomError>;
pub fn load_rom(rom_reader: &mut impl Read) -> LoadRomResult;
pub enum RomError {
Io(std::io::Error),
RomFormat(String),
UnsupportedMapper(u8), // known mapper id, not compiled in
}
pub struct HeaderData {
pub prg_rom_size: u64,
pub chr_rom_size: u64,
pub mirroring: Mirroring,
pub has_persistent_ram: bool,
pub has_trainer: bool,
pub mapper: u8,
pub vs_unisystem: bool,
pub playchoice: bool,
pub ines2: bool,
pub prg_ram_size: u16,
}
pub enum Mirroring { Horizontal, Vertical, FourScreen }
```
**Supported mappers**:
| 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 `ines2` flag 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:
```bash
cargo run --bin ines_header_inspector
```
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
- **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_log` feature 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 `perf` or `flamegraph`
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](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.