# 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: - [NES-specific documentation](README-NES.md) - [Game Boy-specific documentation](README-GB.md) - [Game Boy Advance-specific documentation](README-GBA.md) - [SNES-specific documentation](README-SNES.md) ## Installation ### Pre-built releases Download the latest archive for your platform from [GitHub Releases](https://github.com/rmstdope/neser/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: ```bash 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 ```bash 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](https://rustup.rs); 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: ```bash cargo build --release --bin neser ``` Native desktop run from source: ```bash cargo run --release --bin neser -- [OPTIONS] [ROM] ``` Examples: ```bash 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: ```bash 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: ```bash neser --help ``` Common examples: ```bash 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. ```bash 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 `_.png`, with N zero-padded to the width of `--frames`, and `--output` still receives the final frame. ```bash 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`. ## 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](README-SNES.md). See [web/README.md](web/README.md) for detailed prerequisites, build, run, and test commands. Convenience commands: ```bash bash scripts/build_web.sh bash scripts/run_web.sh ``` `scripts/run_web.sh` serves the built `dist/` directory at (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 ` if specified 5. Command-line arguments Copy the example config to get started: ```bash mkdir -p ~/.neser cp neser.conf.example ~/.neser/neser.conf ``` See [neser.conf.example](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: - [README-NES.md](README-NES.md) - [README-GB.md](README-GB.md) - [README-GBA.md](README-GBA.md) - [README-SNES.md](README-SNES.md) ## Development Recommended setup after cloning: ```bash 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](https://github.com/rmstdope/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: ```bash 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](https://googlechromelabs.github.io/chrome-for-testing/). Useful checks on their own: ```bash 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: ```bash ./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](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`](scripts/pyproject.toml), which holds the ruff and mypy settings and pins the dependencies as [PEP 735][pep735] dependency groups. Create the virtualenv once, from the repository root: ```bash ./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: ```bash 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. [pep735]: https://peps.python.org/pep-0735/ ## 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](docs/VENDOR_SUBMODULES.md) for when and how to move the pin, and how to re-verify shader reachability afterwards. - [architecture.md](architecture.md) describes the codebase structure in more detail. ## License NESER is licensed under the MIT License. See [LICENSE](LICENSE).