Skip to content

Latest commit

 

History

4,186 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NESER

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:

Installation

Pre-built releases

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
./neser

On 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.

Install from crates.io

cargo install neser

No 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.

Building from source

Prerequisites:

  • Rust via rustup; the exact toolchain is pinned in rust-toolchain.toml and rustup installs it, with clippy, rustfmt and the wasm target, on the first cargo run
  • Platform build tools needed by Rust native dependencies
  • Node.js 20+ and npm for the web frontend

Native desktop build:

cargo build --release --bin neser

Native 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.sfc

The repository contains multiple binaries, so include --bin neser when using cargo run.

Running NESER

NESER can be launched with or without a ROM path:

neser
neser path/to/rom

When 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 --help

Common examples:

neser --fullscreen path/to/rom
neser --no-audio path/to/rom
neser --config path/to/neser.conf path/to/rom

Headless frame capture

--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.png

Each 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.

Timing traces against Mesen2

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".

Web frontend

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.sh

scripts/run_web.sh serves the built dist/ directory at http://localhost:8000 (or on the port in NESER_WEB_PORT).

Configuration

NESER can be configured with config files and command-line arguments.

Configuration priority is:

  1. Built-in defaults
  2. ~/.neser/neser.conf
  3. ./neser.conf
  4. --config <file> if specified
  5. Command-line arguments

Copy the example config to get started:

mkdir -p ~/.neser
cp neser.conf.example ~/.neser/neser.conf

See neser.conf.example for all documented configuration keys and valid values.

Controls

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:

Development

Recommended setup after cloning:

git config core.hooksPath .githooks

The 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 scripts

Run 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.

Bumping the Rust toolchain

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:

  1. Change channel in rust-toolchain.toml to the new exact version (for example 1.99.0).
  2. Run ./scripts/gate-full.sh; rustup installs the new toolchain on the first cargo call.
  3. 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.

Python tooling

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/activate

The 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 scripts

CI 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.

Repository guide

  • 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/ and vendor/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.

License

NESER is licensed under the MIT License. See LICENSE.

Releases

Packages

Contributors

Languages