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
- 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-scaleforces 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_headlesstool for deterministic regression testing in CI
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.
brew install cmake sdl2 freetype
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jsudo apt install build-essential cmake libsdl2-dev libfreetype-dev
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jlibfreetype-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 -jCMake 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.
Passing a ROM directly keeps the one-shot startup:
./build/host/allnes path/to/game.nesWith 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.
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 |
ctest --test-dir build --output-on-failureTest 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.ppmThe 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 5The 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 2AllNES/
├── 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
- 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.
This project is released under GPL-3.0; see LICENSE.


