NESER is a Nintendo emulation systems engine written in Rust. It provides native desktop and browser frontends for multiple emulated systems, with shared platform code for audio, video, input, save states, configuration, ROM browsing, and debugging.
Supported emulation targets:
- Nintendo Entertainment System / Famicom / related variants
- Game Boy and Game Boy Color
- Game Boy Advance
- Super Nintendo Entertainment System (SNES)
For emulator-specific options and notes, see:
- NES-specific documentation
- Game Boy-specific documentation
- Game Boy Advance-specific documentation
- SNES-specific documentation
Download the latest archive for your platform from GitHub Releases.
Each release's notes are also in docs/releases/. Releases are cut with the release skill
(.claude/skills/release/SKILL.md): it computes the next version (maintenance, minor or major),
drafts the notes and the web frontend's scroll text from what merged since the previous tag, lands
them through a pull request once approved, and pushes the tag that runs the release workflow.
Release archives contain a top-level neser/ directory. Extract the archive, enter that directory, and run the included binary:
cd neser
./neser --version
./neserOn Windows, run neser.exe --version and neser.exe from the extracted neser\ directory.
Release packages include the native emulator binary, runtime assets, shader presets required by configured presets, neser.conf.example, README.md, the system-specific README files, and LICENSE.
cargo install neserNo SDL2 setup is required. The native frontend uses Rust crates for windowing, audio, and gamepad input. Linux source builds may still require system development packages for backend libraries used by those crates, such as ALSA and Wayland/X11.
Prerequisites:
- Rust via rustup; the exact toolchain is pinned in
rust-toolchain.tomland rustup installs it, with clippy, rustfmt and the wasm target, on the firstcargorun - Platform build tools needed by Rust native dependencies
- Node.js 20+ and npm for the web frontend
Native desktop build:
cargo build --release --bin neserNative desktop run from source:
cargo run --release --bin neser -- [OPTIONS] [ROM]Examples:
cargo run --release --bin neser --
cargo run --release --bin neser -- path/to/game.nes
cargo run --release --bin neser -- path/to/game.gb
cargo run --release --bin neser -- path/to/game.gba
cargo run --release --bin neser -- path/to/game.sfcThe repository contains multiple binaries, so include --bin neser when using cargo run.
NESER can be launched with or without a ROM path:
neser
neser path/to/romWhen launched without a ROM path, NESER opens the ROM browser. When launched with a ROM path, it loads that ROM directly.
Use the built-in help for the complete current CLI reference:
neser --helpCommon examples:
neser --fullscreen path/to/rom
neser --no-audio path/to/rom
neser --config path/to/neser.conf path/to/rom--headless runs a ROM without opening a window and writes a single frame to a
PNG, then exits. It works for all four systems and is intended for scripting and
CI — comparing a capture against a reference image, for example.
neser --headless --output shot.png path/to/rom # 60 frames (default)
neser --headless --frames 300 --output shot.png path/to/rom--output is required, and --frames, --capture-every and --output are rejected
without --headless. The exit code is 0 on success and 1 with a message on failure, so a
script can branch on it.
To capture a series of checkpoints in one run, add --capture-every K: every frame that
is a multiple of K is also written next to --output as <stem>_<N>.png, with N
zero-padded to the width of --frames, and --output still receives the final frame.
neser --headless --frames 3600 --capture-every 300 --output out/shot.png path/to/rom
# writes out/shot_0300.png, out/shot_0600.png, ..., out/shot_3600.png and out/shot.pngEach checkpoint is byte-identical to a single-frame capture at the same --frames, so a
sweep against a reference emulator (see scripts/reference_capture/README.md) costs one
run per ROM instead of one per checkpoint. --capture-every must be at least 1 and at
most --frames. Existing files at those paths are overwritten, as --output is.
Captures are reproducible: the mode forces zero-initialised RAM, takes no input,
and refuses to combine with the autorun flags or --tui. The same ROM and frame
count produce byte-identical PNGs across runs and across builds.
NES captures use the same palette as the window: --nes-palette (or nes-palette= in the config file) applies. Pass --nes-palette mesen to compare against Mesen2. Original Game Boy captures likewise use --gb-palette (or gb-palette=), and grey when neither is set; on Game Boy Color hardware they use --gbc-palette (or gbc-palette=), and auto when neither is set.
Note that a ROM may still be showing a blank screen in its first frames — the
default of 60 is enough for the test ROMs in roms/, but a ROM with a longer
boot or intro sequence needs a larger --frames.
When an NES or SNES game drifts from Mesen2, timing_trace (cargo build --release --features native --bin timing_trace) writes NESER's half of two traces: the clock at each
NMI entry, and every instruction between two NMI entries. The Mesen2 scripts in
scripts/reference_capture/ write the other half, and python -m scripts.diff_timing_traces
names the first divergent line. The recipe is in scripts/reference_capture/README.md,
"Tracing against Mesen2".
The browser frontend is built from the Rust WASM target and the JavaScript frontend under web/.
Supported ROM extensions in the web frontend include .nes, .gb/.gbc/.cgb, .gba, and SNES .sfc/.smc.
SNES games with a DSP-1, DSP-2, DSP-3 or DSP-4 chip (Super Mario Kart, Pilotwings, Dungeon
Master, SD Gundam GX, Top Gear 3000) need the chip's genuine firmware, which you supply: on the
desktop put dsp1b.rom, dsp2.rom, dsp3.rom or dsp4.rom in ~/.neser/firmware (or set snes-firmware-dir); the browser version
asks for each chip's file once. See README-SNES.md.
See web/README.md for detailed prerequisites, build, run, and test commands.
Convenience commands:
bash scripts/build_web.sh
bash scripts/run_web.shscripts/run_web.sh serves the built dist/ directory at http://localhost:8000 (or on the port in NESER_WEB_PORT).
NESER can be configured with config files and command-line arguments.
Configuration priority is:
- Built-in defaults
~/.neser/neser.conf./neser.conf--config <file>if specified- Command-line arguments
Copy the example config to get started:
mkdir -p ~/.neser
cp neser.conf.example ~/.neser/neser.confSee neser.conf.example for all documented configuration keys and valid values.
The native frontend supports keyboard and gamepad input.
Common hotkeys:
| Key | Action |
|---|---|
Space |
Pause/resume |
Escape |
Release mouse grab / close overlays |
Ctrl+Q |
Quit |
Ctrl+R |
Reset |
Ctrl+F |
Toggle fullscreen |
F1 |
Toggle FPS overlay |
F2 / F3 |
Volume up/down |
F4 |
Cycle shader preset |
F5 |
Toggle debugger |
F6 / F7 |
Save/load state |
F8 |
Cycle the palette (NES system palette, or the original Game Boy's shade palette or Game Boy Color tints), or switch Game Boy Color or Game Boy Advance colour correction |
F10 / F11 |
Debugger step over/into |
System-specific controls and controller options are documented in:
Recommended setup after cloning:
git config core.hooksPath .githooksThe hooks in .githooks auto-format staged Rust and Python files (Python with the checkout's
.venv/bin/ruff, or ruff on PATH; a commit with staged Python and no ruff is refused), then
forward to the beads hooks under .beads/hooks that keep the work board in step with git.
Running bd init again will point core.hooksPath at .beads/hooks; set it back to
.githooks afterwards so both keep running.
Working with the fleet: development is driven by a fleet of AI agents run by
Cerebro, and planned work is tracked as beads (bd) on a
board that syncs through a Dolt remote on this repository, not through git. After cloning:
git submodule update --init --recursive # cerebro, shaders and SNES test ROMs
bd bootstrap # fetch the work board (refuses if .beads has a database already)
bd list # see it; bd dolt pull / bd dolt push keep it in sync
.cerebro/cerebro/scripts/cerebro-tui # the fleet view (needs cargo)CLAUDE.md describes how work moves; docs/migration/github-to-beads.md records how the GitHub
issues were moved onto the board. GitHub issues remain open as the inbox for bug reports and
requests from outside.
The full pre-merge gate is one script, ./scripts/gate-full.sh (--fast for the quick subset).
Before its long legs it runs scripts/chromedriver_match.py, which picks a ChromeDriver whose major
version matches the installed Chrome (any chromedriver on PATH first, then wasm-pack's cache) and
hands it to wasm-pack test --chromedriver; with none it stops at once with one line naming both
versions. The fix is a matching ChromeDriver first on PATH, e.g. from
Chrome for Testing.
Useful checks on their own:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --target wasm32-unknown-unknown --no-default-features --features wasm --all-targets -- -D warnings
cargo clippy --no-default-features --features frontend --all-targets -- -D warnings
cargo test --all-features --lib
npm test
sh scripts/build_web.sh --no-bundle && npx tsc --noEmit -p tsconfig.json
ruff check scripts
ruff format --check scripts
mypy --config-file scripts/pyproject.toml scriptsRun tests for a specific Rust source area:
./scripts/test-dir.sh src/gb --skip-integration
./scripts/test-dir.sh src/nes/cartridge
./scripts/test-dir.sh src/snes--skip-integration skips the integration-test modules of all consoles
(nes/gb/gba/snes) for fast iteration; run without it before creating a
PR. SNES test commands, asset policy, and the golden-baseline approval
workflow are documented in README-SNES.md.
Local builds and CI use the one Rust version pinned in rust-toolchain.toml, so a new stable
release cannot turn CI red on unchanged code. Moving to a newer Rust is a deliberate pull request:
- Change
channelinrust-toolchain.tomlto the new exact version (for example1.99.0). - Run
./scripts/gate-full.sh; rustup installs the new toolchain on the firstcargocall. - Fix every new clippy lint and rustfmt change in the same pull request.
RUSTUP_TOOLCHAIN in the environment beats the file. rustup's cargo sets it for every process
it starts, so a shell opened from a program launched with cargo run inherits stable.
scripts/gate-full.sh, scripts/test-dir.sh, scripts/build_web.sh and the pre-commit hook
unset it. For any other command, run unset RUSTUP_TOOLCHAIN first, and check with rustup show active-toolchain.
The pin uses rustup's minimal profile, so CI downloads no more than it needs. rust-analyzer needs
the standard library source: run rustup component add rust-src once per pinned version.
The tools under scripts/ are configured by
scripts/pyproject.toml, which holds the ruff and
mypy settings and pins the dependencies as PEP 735 dependency groups.
Create the virtualenv once, from the repository root:
./scripts/setup-venv.sh # or PYTHON=python3.14 ./scripts/setup-venv.sh
source .venv/bin/activateThe script creates .venv if it is missing and installs the test and dev
groups exactly as CI does. Every Cerebro-prepared worktree runs it on install.
scripts/gate-full.sh uses .venv/bin/python and never the system python3,
so the full gate stops at once, with one line naming this script, when .venv
is missing.
The test group holds the runtime dependencies of the tools; dev holds ruff
and mypy. A third group, deploy, is only needed for scripts/deploy.py.
Installing the groups requires pip 25.1 or newer.
Then, with the virtualenv active:
python -m unittest discover -s scripts -t . -p "test_*.py"
ruff check scripts
ruff format --check scripts
mypy --config-file scripts/pyproject.toml scriptsCI runs exactly these four commands on Python 3.14. ruff format rewrites in
place, so enabling the pre-commit hook above keeps the formatting check green
without thinking about it. mypy is silent by default and strict only for the
shared data-access modules listed in scripts/pyproject.toml; adding a module
to that list is how type coverage grows.
src/contains Rust emulator, frontend, and shared platform code.web/contains the browser frontend.roms/contains automated test ROMs and test assets.scripts/contains build, packaging, test, ROM management, and metadata tools.shaders/andvendor/slang-shaders/contain shader presets used by the native frontend.vendor/slang-shaders/is a pinned submodule; see docs/VENDOR_SUBMODULES.md for when and how to move the pin, and how to re-verify shader reachability afterwards.- architecture.md describes the codebase structure in more detail.
NESER is licensed under the MIT License. See LICENSE.