fogleman/nes
NES emulator written in Go.
Stale — last commit 2y ago
weakest axislast commit was 2y ago; no tests detected…
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
last commit was 2y ago; no CI workflows detected
- ✓13 active contributors
- ✓MIT licensed
- ⚠Stale — last commit 2y ago
Show all 6 evidence items →Show less
- ⚠Single-maintainer risk — top contributor 82% of recent commits
- ⚠No CI workflows detected
- ⚠No test directory detected
What would change the summary?
- →Use as dependency Mixed → Healthy if: 1 commit in the last 365 days; add a test suite
- →Deploy as-is Mixed → Healthy if: 1 commit in the last 180 days
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Forkable" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/fogleman/nes)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/fogleman/nes on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: fogleman/nes
Generated by RepoPilot · 2026-05-09 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/fogleman/nes shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
WAIT — Stale — last commit 2y ago
- 13 active contributors
- MIT licensed
- ⚠ Stale — last commit 2y ago
- ⚠ Single-maintainer risk — top contributor 82% of recent commits
- ⚠ No CI workflows detected
- ⚠ No test directory detected
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live fogleman/nes
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/fogleman/nes.
What it runs against: a local clone of fogleman/nes — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in fogleman/nes | Confirms the artifact applies here, not a fork |
| 2 | License is still MIT | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 658 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of fogleman/nes. If you don't
# have one yet, run these first:
#
# git clone https://github.com/fogleman/nes.git
# cd nes
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of fogleman/nes and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "fogleman/nes(\\.git)?\\b" \\
&& ok "origin remote is fogleman/nes" \\
|| miss "origin remote is not fogleman/nes (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
&& ok "license is MIT" \\
|| miss "license drift — was MIT at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "main.go" \\
&& ok "main.go" \\
|| miss "missing critical file: main.go"
test -f "nes/console.go" \\
&& ok "nes/console.go" \\
|| miss "missing critical file: nes/console.go"
test -f "nes/cpu.go" \\
&& ok "nes/cpu.go" \\
|| miss "missing critical file: nes/cpu.go"
test -f "nes/ppu.go" \\
&& ok "nes/ppu.go" \\
|| miss "missing critical file: nes/ppu.go"
test -f "nes/mapper.go" \\
&& ok "nes/mapper.go" \\
|| miss "missing critical file: nes/mapper.go"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 658 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~628d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/fogleman/nes"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
A fully functional NES (Nintendo Entertainment System) emulator written in pure Go that executes authentic 6502 CPU instructions, emulates PPU graphics rendering, and APU audio synthesis. It supports 6 major cartridge mappers (NROM, MMC1, UNROM, CNROM, MMC3, AOROM) covering ~85% of the NES game library, with a GL-based UI for rom selection and real-time gameplay. Monolithic package structure: nes/ contains the core emulation (cpu.go, ppu.go, apu.go, memory.go, 6 mapper implementations), ui/ wraps the GL frontend (gameview, menuview, texture rendering), and main.go orchestrates rom loading and event loops.
👥Who it's for
Emulation enthusiasts and retro gaming hobbyists who want to play NES games on modern systems; Go developers interested in learning systems programming, CPU/GPU cycle-accurate emulation, or reverse-engineering hardware behavior from documentation.
🌱Maturity & risk
Actively developed but stable—the single-maintainer project (fogleman) is well-established in the emulation community with functional gameplay on most games. No visible CI/test suite in the file list, and documented known issues (minor PPU timing problems, imperfect APU), so it's reliable for gameplay but not production-grade for accuracy research.
Single maintainer (fogleman) with minimal test coverage visible in the repo structure. Dependency surface is small (3 external Go packages: portaudio, GLFW, OpenGL) but portaudio requires system C libraries (PortAudio dev headers), creating a build barrier on some systems. No recent commit data available, so current maintenance status is unclear.
Active areas of work
No active PR/issue data provided in the repo metadata, but the README indicates the maintainer has documented known PPU timing and APU accuracy gaps as future work. The mappers list suggests the next phase would add support for less common cartridge types.
🚀Get running
brew install portaudio # macOS; use apt-get for Ubuntu
go get github.com/fogleman/nes
nes [path/to/rom/directory or rom_file.nes]
Daily commands:
go run main.go [rom_directory] # Launches menu UI to select game
go run main.go path/to/game.nes # Directly loads a rom
🗺️Map of the codebase
main.go— Entry point that initializes the UI runtime and loads ROM files; every contributor must understand the initialization flow.nes/console.go— Core emulation harness that ties together CPU, PPU, APU, and memory; the central state machine of the entire emulator.nes/cpu.go— 6502 CPU implementation with instruction execution and cycle-accurate timing; fundamental to all game logic.nes/ppu.go— Picture Processing Unit responsible for rendering graphics and generating video frames; critical for display output.nes/mapper.go— Cartridge memory mapper abstraction that routes memory access; different mappers (mapper1.go, mapper2.go, etc.) implement game-specific hardware.ui/run.go— Bridges the emulation core with the OpenGL/GLFW UI runtime; orchestrates the main render loop.nes/ines.go— iNES ROM file parser and cartridge loader; enables any ROM file to be loaded and validated.
🛠️How to make changes
Add support for a new cartridge mapper
- Create a new mapper file (e.g., nes/mapper5.go) implementing the Mapper interface with Read/Write/Step methods (
nes/mapper5.go) - Register the mapper in the cartridge loader to match ROM header mapper ID (
nes/cartridge.go) - Implement PRG/CHR bank switching logic and mirror mode handling specific to that mapper's hardware (
nes/mapper5.go)
Add a new CPU instruction or fix CPU behavior
- Locate the instruction handler in the CPU switch statement (organized by opcode) (
nes/cpu.go) - Implement or update the addressing mode function and cycle timing (
nes/cpu.go) - Test with known game ROMs to validate behavior against hardware traces (
nes/cpu.go)
Add a new APU channel or sound feature
- Define the channel state and update the APU struct (
nes/apu.go) - Implement the Step() method for channel timing and output generation (
nes/apu.go) - Connect write handlers for control registers (0x4000–0x4017) (
nes/apu.go) - Integrate PCM output into the audio buffer sent to PortAudio (
ui/audio.go)
Add a new UI menu or game selection feature
- Create a new view struct and implement the View interface (Draw, Update, Handle methods) (
ui/menuview.go) - Register the view in the Director state machine (
ui/director.go) - Add button rendering and input handling for navigation (
ui/util.go)
🔧Why these technologies
- Go — Lightweight, cross-platform, simple concurrency; suited for cycle-accurate emulation without GC pauses in tight loops (used selectively).
- OpenGL (via go-gl) — Hardware-accelerated 2D rendering for the NES framebuffer; allows efficient real-time texture updates and scaling.
- GLFW — Cross-platform window management and input handling for keyboard/gamepad input with minimal abstraction.
- PortAudio — Cross-platform audio output abstraction; handles low-latency PCM playback on macOS, Linux, Windows.
⚖️Trade-offs already made
-
Single-threaded emulation loop tightly coupled to UI/rendering
- Why: Simplifies synchronization between CPU, PPU, APU; avoids race conditions in shared console state.
- Consequence: UI responsiveness depends on emulation speed; blocked emulation stalls the window. No background ROM loading or multithreaded rendering.
-
Cycle-accurate PPU scanning per-line vs. frame-buffering
- Why: Precise sprite and tile rendering, correct mid-scanline behavior (raster effects), and accurate V-blank timing.
- Consequence: Higher CPU overhead per frame; better compatibility with edge-case games but slower on weak systems.
-
Mapper abstraction with polymorphism vs. runtime dispatch table
- Why: Clean separation of mapper logic; different ROM layouts have wildly different bank-switching behavior.
- Consequence: Small code overhead per memory access; requires explicit mapper registration per ROM type.
-
Fixed-point integer arithmetic for APU synthesis
- Why: Avoids floating-point overhead; matches period/frequency calculations used in hardware.
- Consequence: Audio quality is accurate but lower resolution than floating-point; envelope/decay must be calibrated carefully.
🚫Non-goals (don't propose these)
- Does not support NES 2.0 ROM header format (only iNES 1.0).
- Does not implement all 256 mappers; only covers ~10 most common.
- Does not provide save state serialization or rewind; console state is ephemeral.
- Does not handle Famicom Disk System (FDS) games or ROM patches.
- Does not support network play or TAS (Tool-Assisted Speedrun) recording.
- Does not provide debugger, profiler, or instruction tracer in UI.
🪤Traps & gotchas
PortAudio native dependency: Build will fail without system C libraries (portaudio19-dev on Ubuntu, portaudio via Homebrew on macOS)—Go's go get cannot auto-resolve this. No headless mode: The emulator requires an OpenGL context; you cannot run roms in pure CLI. Mapper complexity: Each mapper has quirky bus behavior (e.g., MMC3 scanline counters, MMC1 shift register state); subtle bugs in mappers can cause games to hang or glitch silently.
🏗️Architecture
💡Concepts to learn
- Cycle-Accurate Emulation — This emulator steps CPU, PPU, and APU in lockstep to match real NES clock cycles; understanding cycle timing is critical for debugging synchronization bugs and why some games glitch.
- Bank Switching / Cartridge Mappers — NES cartridges use mappers to expand addressable ROM beyond the CPU's 16-bit address space; each mapper (MMC1, MMC3, etc.) has different register behavior that the emulator must faithfully replicate.
- Memory-Mapped I/O — The NES exposes CPU, PPU, and APU control registers at fixed memory addresses; reads/writes to these addresses trigger hardware side-effects, not just RAM access.
- Scanline Rendering Pipeline — The PPU generates one scanline of pixels per 341 CPU cycles; games exploit mid-scanline register changes for effects like split-screen scrolling, so emulation must interleave CPU and PPU execution.
- 6502 Addressing Modes — The 6502 CPU supports 13 different addressing modes (immediate, zero-page, absolute, indexed, indirect); the emulator must decode and execute each instruction's operand fetch correctly.
- Shift Register State Machines (MMC1) — MMC1 cartridges use a 5-bit shift register to serialize bank-switch commands; the emulator must track shift state across multiple writes, a common source of subtle bugs.
- Sprite-0 Hit Detection — Games rely on PPU status register bit 6 (sprite-0 hit) for precise timing and raster effects; incorrect hit detection can cause visual glitches or game hangs.
🔗Related repos
jmoon018/NesEmulator— C++ NES emulator with focus on cycle-accuracy; useful for cross-referencing CPU/PPU behavior against a different implementationOneLoneCoder/NES_Emulator— Another popular learning-focused NES emulator (C++) with detailed video tutorials; good for understanding the same design decisions in a different language6502ts/6502.ts— TypeScript 6502 CPU emulator with visual debugger; helpful for isolating CPU bugs separate from PPU/mapper interactionsnesdev-wiki/NesDoc— Canonical NES hardware documentation in repo form; required reading for understanding cartridge mappers and register behavior
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add unit tests for CPU instruction execution (nes/cpu.go)
The CPU is the core of the emulator, but there are no unit tests visible in the repo structure. Testing individual CPU opcodes against known instruction cycles and state changes would catch regressions early. This is critical for an emulator where correctness is paramount.
- [ ] Create nes/cpu_test.go with test cases for representative opcodes (LDA, JMP, JSR, RTS, etc.)
- [ ] Test that opcodes correctly modify accumulator, X, Y, program counter, and status flags
- [ ] Verify cycle counts match official NES CPU documentation for each instruction
- [ ] Add tests for addressing modes (immediate, absolute, indirect, etc.) in nes/memory.go interactions
Add unit tests for mapper implementations (nes/mapper*.go)
The repo has 8 different mapper implementations (mapper1-225) that translate ROM addresses to physical memory. Without tests, mapper bugs are hard to catch and likely cause game-specific glitches. Each mapper should have basic validation.
- [ ] Create nes/mapper_test.go to test Read/Write operations for each mapper variant
- [ ] Test bank switching logic in mapper1.go (MMC1), mapper4.go (MMC3), and others
- [ ] Verify that mapper3.go (CNROM) correctly handles CHR-ROM bank selection
- [ ] Add tests for edge cases like mirror address ranges and boundary conditions
Complete missing README documentation for usage and game compatibility (README.md)
The README is incomplete (cuts off mid-sentence at 'If no arguments are specified, the program will') and lacks important user information like what games are tested/compatible, keyboard controls, and troubleshooting steps. This impacts new user experience.
- [ ] Complete the cut-off sentence about default behavior when no arguments provided
- [ ] Add keyboard/controller input mapping section (reference nes/controller.go implementation)
- [ ] Add a 'Known Working Games' or 'Compatibility' section listing tested ROMs
- [ ] Add troubleshooting section for common PortAudio build issues on different platforms
- [ ] Document how to enable/disable audio and any performance tuning options
🌿Good first issues
- Add unit tests for the 6502 CPU instruction set in nes/cpu_test.go—pick one opcode (e.g., ADC addressing modes) and verify it matches nesdev.com reference behavior.
- Improve mapper test coverage: create nes/mapper_test.go with golden-file tests for known ROM checksums (e.g., 'Super Mario Bros uses MMC0, expect sprite X at frame 10 = 64'); currently no test fixtures exist.
- Document the PPU scanline state machine in nes/ppu.go with ASCII diagrams of timing—the README acknowledges 'minor PPU timing issues'; a clear diagram would help future contributors identify the gap.
⭐Top contributors
Click to expand
Top contributors
- @fogleman — 82 commits
- @zhaostu — 4 commits
- @TianZerL — 2 commits
- @mattkanwisher — 2 commits
- @ncoish — 2 commits
📝Recent commits
Click to expand
Recent commits
3880f34— Merge pull request #55 from alanoliveira/master (fogleman)cf2994c— fix apu framePeriod initialization (alanoliveira)d484bd9— snapshot save/load with number keys (fogleman)088b5ce— Merge pull request #51 from TianZerL/master (fogleman)1a895f3— adjust code format (TianZerL)b834c33— apu fix (TianZerL)0aace4b— Merge pull request #47 from mtojek/fix-oam-and-e3 (fogleman)92cf046— Fix: emulate unimplemented bits of sprite's byte 2 (mtojek)a5786ee— Merge pull request #46 from odidev/nes_arm64 (fogleman)44b93df— Update glfw to v3.2 to add Linux/ARM64 support (odidev)
🔒Security observations
The NES emulator project has moderate security concerns, primarily stemming from significantly outdated dependencies and Go runtime version (1.14 from 2020). The lack of input validation on ROM files and binary parsing represents a medium-severity risk. While this is a desktop application with limited network exposure, addressing the dependency updates and adding file/input validation would substantially improve the security posture. The project would benefit from modern Go tooling, dependency management best practices, and fuzzing tests for binary parsing robustness.
- High · Outdated Go Version —
go.mod. The project specifies Go 1.14 in go.mod, which is significantly outdated (released in March 2020). This version is no longer receiving security updates and may contain unpatched vulnerabilities in the Go runtime itself. Fix: Update to Go 1.21 or later (preferably the latest stable version) to receive security patches and performance improvements. - High · Outdated Direct Dependencies —
go.mod. All three direct dependencies use very old pinned versions from 2018-2020 without version constraints. These packages may contain known security vulnerabilities. Specifically: github.com/go-gl/gl (2019), github.com/go-gl/glfw (2020), and github.com/gordonklaus/portaudio (2018). Fix: Update dependencies to their latest versions or use semantic versioning (e.g., 'v1.0.0' or '^1.0.0'). Run 'go get -u' and 'go mod tidy', then audit with 'go list -json -m all | nancy sleuth' or similar tools. - Medium · Missing Native Library Dependency Validation —
README.md, main.go. The project depends on PortAudio native library (portaudio19-dev on Linux, portaudio via homebrew on macOS). There is no verification mechanism to ensure the correct version is installed or that the system has the required libraries. This could lead to runtime failures or the use of vulnerable native library versions. Fix: Add build-time validation to check for required native libraries and their versions. Consider documenting minimum versions required for PortAudio and other native dependencies. - Medium · Potential Unsafe File Input Handling —
main.go, util/roms.go, nes/cartridge.go. The program accepts ROM files as command-line arguments and loads them directly. Without evidence of file validation, path traversal, or file size limits, this could be exploited to read arbitrary files or cause denial of service (e.g., loading extremely large files). Fix: Implement strict file validation: verify file existence, check file size limits, validate file format/magic bytes, and use filepath.Clean() to prevent path traversal attacks. - Medium · No Input Validation on ROM Data —
nes/ines.go, nes/cartridge.go. The iNES parser (nes/ines.go) likely processes untrusted binary ROM data. Without proper bounds checking and validation, malformed ROM files could trigger buffer overflows, integer overflows, or logic errors in the emulator. Fix: Add comprehensive input validation: check header magic bytes, validate sizes before allocating memory, use safe parsing methods, and implement fuzzing tests to detect crash-inducing inputs. - Low · Missing Security Headers in UI —
ui/gameview.go, ui/menuview.go. The UI components (ui/gameview.go, ui/menuview.go) handle user-facing content but there's no evidence of security headers or content validation that could prevent potential XSS if web components are introduced later. Fix: If web-based UI is ever added, implement Content Security Policy (CSP) headers and validate all user-supplied content. Use UI frameworks that auto-escape by default. - Low · No Logging of Security Events —
Project-wide. There is no evidence of security event logging (invalid ROM files, parsing failures, etc.), making it difficult to detect or investigate potential attacks. Fix: Add logging for security-relevant events: file access attempts, parsing failures, invalid inputs. Use structured logging and consider different log levels.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.