Skip to content
UnmetDecadePublic

About

A cross-platform NES (Famicom) emulator written in C++17, with a platform-agnostic core and a replaceable front-end layer.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AllNES

English · 简体中文

A cross-platform NES (Famicom) emulator written in C++17. It is built around a platform-independent core and a replaceable frontend layer; the Linux and macOS desktop frontends use SDL2.

  • Frontend: SDL2 (macOS / Linux / Windows)
  • Core: pure C++17, with no platform headers
  • Built, run, and tested on Ubuntu 22.04 (g++ 11.4, CMake 3.22) and macOS

Screenshots

Launcher home: selected game banner, recently played, and a library preview

Settings page: library, display, input and controllers, shortcuts, covers, and about

Game library grid: covers, mapper, and file size

Features

  • 6502 CPU, scanline-based PPU, APU (two pulse channels, triangle, noise, DMC)
  • NMI / IRQ, OAM DMA, DMC DMA
  • iNES format; mappers: NROM(0), MMC1/SxROM(1), UxROM(2), CNROM(3), MMC3(4), AxROM(7), ColorDreams(11), GxROM(66), and MMC3 variants (245, for Chinese-patched cartridges such as Dragon Quest)
  • Two-player keyboard input, plus support for up to two controllers (hot-plug, one per player); bundled SDL community mapping database (2200+ entries). Third-party controllers (GameSir, 8BitDo in X-input mode, etc.) that aren't in the database automatically fall back to the generic Xbox mapping, with any missing D-pad directions filled in
  • Audio: APU native sample rate is about 894 kHz, resampled by the frontend to the device sample rate
  • Aspect-ratio-preserving scaling: by default fills the window or screen along the constraining dimension (fullscreen fills the height and leaves pillarboxes on the sides); --integer-scale forces integer multiples; fullscreen, mute, and frame-rate control
  • Graphical launcher: dark card grid, cover banner, recently played, live search, resolution switching, and a details drawer that slides out on demand; the UI is built on LVGL 10 and rendered through a separate display abstraction (SDL2 and headless backends), independent of the emulator core
  • The LVGL UI loads system CJK fonts directly through FreeType; the display layer can be swapped for SDL2 / headless, and future backends such as OpenGL or FFmpeg can be added without changing the UI
  • Covers are read directly from PNG/JPEG (stb_image built in, no SDL2_image dependency); games without a cover get a pixel-art placeholder generated from the ROM name
  • Windowless allnes_headless tool for deterministic regression testing in CI

Building

Requirements: CMake ≥ 3.12, a C++17-capable compiler, and SDL2. The launcher UI additionally needs FreeType (font rendering); if LVGL/FreeType aren't found the launcher isn't built, but passing a ROM directly still runs the emulator.

macOS

brew install cmake sdl2 freetype
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

Ubuntu / Debian

sudo apt install build-essential cmake libsdl2-dev libfreetype-dev
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

libfreetype-dev is for the LVGL UI (enabled by default; turn it off with -DALLNES_WITH_LVGL=OFF). The launcher does not use SDL2_ttf.

If you don't have root, you can extract the libsdl2-dev package into your home directory and point CMake at it:

apt-get download libsdl2-dev
dpkg-deb -x libsdl2-dev_*.deb "$HOME/opt/sdl2"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DSDL2_ROOT="$HOME/opt/sdl2/usr"
cmake --build build -j

CMake prefers SDL2's CMake package; when the absolute paths in the package are stale, it falls back to locating the headers and libraries directly through SDL2_ROOT.

Running

Passing a ROM directly keeps the one-shot startup:

./build/host/allnes path/to/game.nes

With no ROM, the graphical launcher opens:

./build/host/allnes --library "$HOME/roms"
./build/host/allnes --display headless --library "$HOME/roms"   # windowless

--display selects the launcher's display backend: auto (the default, same as sdl), sdl, or headless. The headless backend creates no window; it is meant for CI and offscreen screenshots, and its canvas is fixed at 1920×1080. The launcher scans the library directory (non-recursively) for .nes files. You can also switch libraries from "Settings → Library" by clicking "Choose folder…", which opens the system directory dialog: NSOpenPanel on macOS, IFileOpenDialog on Windows, and zenity or kdialog on Linux (one of them must be installed). The dialog opens at the last location used (the location browsed to when canceled is remembered too), and the selected directory is saved immediately and restored on the next launch. Without --library, the last saved directory is used; on the first run it uses roms/ under the current directory if it exists, otherwise the current directory. The library path, resolution, and most recently launched game are saved in ~/Library/Application Support/AllNES/AllNES/launcher.conf (macOS), $XDG_DATA_HOME/AllNES/AllNES/launcher.conf or ~/.local/share/AllNES/AllNES/launcher.conf (Linux), and %APPDATA%/AllNES/AllNES/launcher.conf (Windows), then restored automatically next time.

The UI is designed on a 1920×1080 logical canvas (default window 1600×900, resizable, minimum 960×540): the top bar holds the Home / Library / Settings tabs and a search box; Home shows a banner for the selected game, a "Recently Played" row, and a row of library previews; the Library page is a 9-column cover grid. The detail panel on the right is hidden by default and slides out as a drawer when you click any game card (or a "Recently Played" card), showing the cover, mapper, file size, run status, resolution, and a launch button; pressing Esc, clicking the × in the top-right corner, clicking anywhere outside the drawer (the top bar, the page, or the bottom status bar), or switching tabs closes it. The arrow keys only move the selection and do not open the panel. The bottom status bar shows the item count, disk usage, and current resolution.

The LVGL UI treats third_party/lvgl/ as a read-only third-party dependency: CMake points LV_BUILD_CONF_PATH at the project's own launcher/lvgl/lv_conf.h (enabling FreeType, the stdio filesystem A:, the CLIB allocator, and PNG/JPEG decoders) and disables examples, demos, ThorVG, and LVGL's own tests.

Between the UI and the window sits the launcher/display/ abstraction layer:

  • The UI only owns the LVGL widget tree, layout, and state machine; it does not include SDL or read events directly;
  • The display layer handles the window/context, dirty-rectangle uploads, present(), event normalization, timing, fullscreen, and pixel readback; the pixel format is uniformly ARGB8888 (0xAARRGGBB);
  • The SDL backend window is 1600×900 points by default and supports HiDPI; on Retina it renders at a 2× canvas (3840×2160) and downsamples, which makes text and covers noticeably sharper;
  • The headless backend opens no window and provides whole ARGB frames through the same interface, for CI screenshots and a future FFmpeg recording backend.

To add a backend, implement the Display interface from launcher/display/display.h, register it in createDisplay(), and extend the --display values; the UI and host need no changes.

Library and Covers

The launcher scans the top level of the --library directory (default roms/) for .nes files. A typical layout looks like this:

roms/
├── 魂斗罗.nes
├── 勇者斗恶龙4.nes
└── images/
    ├── 魂斗罗.png
    └── 勇者斗恶龙4.png

Covers go in the images/ subdirectory of the library directory, named after the ROM, with a .png, .jpg, or .jpeg extension. A 3:4 portrait image is recommended; games without a cover get a pixel-art placeholder generated from the file name.

The repository does not include ROMs or cover images; use game files you legally own.

Launcher keys:

Key Action
Tab Switch between Home / Library / Settings
↑/↓, W/S Move the selection by row (9 items)
←/→, A/D Move the selection one item at a time
PgUp/PgDn, Home/End Page up/down / jump to the first or last item
/ Focus the search box (typing filters live, Backspace deletes)
Enter Launch the selected game
[ / ] Cycle resolution presets (saved immediately)
F5 Rescan the library
F11 Toggle launcher fullscreen
Esc Clear search / leave search / close details drawer / close launcher
Ctrl+Q Quit the whole program
Mouse / touch Hover to preview; click to select and open details; double-click to launch; scroll wheel to scroll

The launcher also supports controller navigation (first controller):

Controller button Action
D-pad / left stick Move the selection (hold for auto-repeat)
A (Xbox) / × (PS) Launch the selected game
B (Xbox) / ○ (PS) Back / close details / quit the launcher
X (Xbox) / □ (PS) Rescan the library
Y (Xbox) / △ (PS) Switch Home / Library / Settings
Back (Xbox) / Share (PS) Toggle launcher fullscreen
Start / Options Launch the selected game
LB / L1, RB / R1 Cycle resolution presets

In-game controller controls:

Controller button NES button
D-pad / left stick Directional pad
A A (jump)
X or B B (run / attack)
Back / Share Select
Start / Options Start

Left-stick D-pad emulation and the dead zone come from "Settings → Input · Controllers" and take effect when a game starts. The "Controller Test" page shows buttons, the four D-pad directions, sticks, triggers, and rumble state in real time.

Controller mappings use the bundled gamecontrollerdb.txt by default (copied next to the executable at build time). Set ALLNES_GAMECONTROLLERDB=/path/to/gamecontrollerdb.txt to use a different mapping database; set ALLNES_GAMEPAD_DEBUG=1 to print the device, GUID, button count, and final mapping string to stderr, which helps diagnose compatibility issues with individual controllers.

Resolution presets (game window size): 3x 768×720 (default), 4x 1024×960, 5x 1280×1200, 1920×1080, 2560×1440, and fullscreen. The game image is still integer-scaled inside the window while preserving the aspect ratio.

Common options:

Option Description
-s, --scale N Window scale factor, default 3
-w, --width N / -H, --height N Specify the window size
-C, --conf FILE Use the specified key bindings file
-L, --library DIR Launcher library directory
--display auto|sdl|headless Select the launcher display backend (default auto)
--fullscreen Start in fullscreen
--integer-scale Integer scaling (every NES pixel the same size, possibly with wider black bars)
--mute-audio Don't open an audio device
--frames N Run N frames then exit (for smoke tests)
--menu-frames N Draw N launcher frames then exit (for smoke tests)

In-game shortcuts: F11 toggles fullscreen, Ctrl+R resets, M toggles mute, and Ctrl+Q quits the whole program. When you enter a game from the launcher, pressing Esc or clicking the game window's close button first opens a confirmation dialog where you can choose Return to launcher, Quit to desktop, or Cancel (keep playing), so closing the window by accident doesn't take the launcher down with it; when launched directly with a ROM, Esc / closing the window still exits immediately.

Default key bindings (editable in keybindings.conf):

NES button Player 1 Player 2
A J Numpad 5
B K Numpad 6
Select Right Shift Numpad 8
Start Enter Numpad 9
Up / Down / Left / Right W / S / A / D Arrow keys

Testing

ctest --test-dir build --output-on-failure

Test ROMs are generated on the fly by tools/make_test_rom.py (a 16 KB PRG + 8 KB CHR NROM program that draws 8-pixel-wide black-and-white vertical stripes), so they don't depend on any copyrighted game.

You can also run the windowless tool manually:

# Check the picture, repeat for determinism, and output a frame hash
./build/tools/allnes_headless --rom build/tools/test_stripes.nes \
    --frames 5 --check-stripes --repeat 2 --hash

# Dump the last frame as a PPM
./build/tools/allnes_headless --rom game.nes --frames 60 --dump-ppm frame.ppm

The launcher_model tests cover ROM header probing, directory scanning and sorting, path expansion, and config read/write; the display_headless tests cover the display interface's dirty-rectangle copy, clipping, pixel format, and lifetime. Neither depends on a window system.

On machines without a display server, you can smoke-test the frontend with the headless backend or SDL's dummy driver:

./build/host/allnes --display headless --library build/tools --menu-frames 5

SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
    ./build/host/allnes --frames 60 --mute-audio build/tools/test_stripes.nes

SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
    ./build/host/allnes --display sdl --library build/tools --menu-frames 5

The launcher supports the following debugging environment variables, handy for headless screenshots and interaction checks:

Variable Description
ALLNES_LAUNCHER_SHOT=FILE Save the launcher frame as a PNG
ALLNES_LAUNCHER_SHOT_FRAME=N Capture at frame N (default frame 0)
ALLNES_LAUNCHER_TAB=0/1/2 Open Home / Library / Settings directly at startup
ALLNES_LAUNCHER_SETTINGS=N Jump directly to section N on the Settings page (2 = Input · Controllers)
ALLNES_LAUNCHER_DETAILS=1 Open the details drawer directly at startup (for screenshots)
ALLNES_LAUNCHER_INPUT_DEBUG=1 Print mouse/touch event coordinates, drawable size, and final canvas coordinates to stderr
ALLNES_GAMEPAD_DEBUG=1 Print the controller device, GUID, button count, and final mapping string
ALLNES_GAMECONTROLLERDB=FILE Use the specified SDL controller mapping database (overrides the built-in one)
ALLNES_LAUNCHER_TAB=1 ALLNES_LAUNCHER_SHOT=/tmp/launcher.png \
    ./build/host/allnes --display headless --library roms --menu-frames 2

Project Layout

AllNES/
├── core/                 # Platform-independent emulator core
│   ├── include/port/     # IVideoSink / IAudioSink / IInputSource
│   ├── include/          # CPU, PPU, APU, main bus, mappers, ...
│   └── src/
├── gamepad/              # SDL controller wrapper: mapping DB, enumeration, hot-plug, generic mapping
├── launcher/             # Standalone launcher layer (does not depend on core)
│   ├── model/            # ROM scanning, iNES header probing, config persistence
│   ├── display/          # Display abstraction: Display interface + SDL2 / headless backends
│   ├── ui/               # LVGL UI layer: widget tree, layout, state machine, screenshots
│   ├── lvgl/             # Project's own lv_conf.h (LVGL configuration)
│   ├── tests/            # Model unit tests + headless display unit tests
│   └── types.h           # LaunchRequest (boundary type between host and UI)
├── host/                 # SDL2 composition root (launcher + emulator main loop)
├── third_party/
│   ├── lvgl/             # Vendored LVGL 10 (kept as-is, sources unmodified)
│   └── stb_image*.h      # Cover decoding and PNG screenshots
├── tools/                # Windowless runner and test ROM generator
├── docs/
│   ├── ARCHITECTURE.md   # Layering and data flow
│   └── images/           # README screenshots
└── keybindings.conf

Known Limitations

  • Only NTSC iNES ROMs are supported; PAL, UNIF, and ROMs with a trainer are rejected.
  • No save states or save RAM, and no battery-backed save persistence.
  • Audio uses simple linear resampling and queue watermark control, not cycle-accurate APU mixing.
  • Some edge-case PPU behavior (for example, the exact timing of turning rendering off or on mid-frame) is approximated.

License

This project is released under GPL-3.0; see LICENSE.

About

A cross-platform NES (Famicom) emulator written in C++17, with a platform-agnostic core and a replaceable front-end layer.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages