Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/skills/nes-hardware-research/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Every hardware research skill in this repository uses the same three tiers. The
Used only when the specification is missing, incomplete, or ambiguous, to see how a specific behavior can be implemented. Implementation evidence is never equal authority with the specification. Where Mesen2 makes a choice the specification does not settle, say so instead of presenting it as hardware fact. No second implementation reference is designated for the NES; the legacy `SourMesen/Mesen` repository is history, not an authority.

3. **Screenshot reference: Mesen2.**
When a visual test ROM needs a reference image, capture it with Mesen2 at the same frame as NESER and pixel-diff the two captures with `python -m scripts.diff_screenshots`. An exact match approves the golden. If the captures differ and Mesen2 itself is suspect, ask the navigator instead of approving either side. The verified headless recipe (Mesen2 release binary in `/Applications/Mesen.app`, testRunner mode, `scripts/reference_capture/mesen2_capture.lua`, `--nes.DisableFrameSkipping=true --nes.RamPowerOnState=AllZeros`) is in `scripts/reference_capture/README.md`; it produced a 0-px match against NESER on instr_test-v5 at frame 120 on 2026-09-25.
When a visual test ROM needs a reference image, capture it with Mesen2 at the same frame as NESER and pixel-diff the two captures with `python -m scripts.diff_screenshots`. An exact match approves the golden. If the captures differ and Mesen2 itself is suspect, ask the navigator instead of approving either side. Run the comparison with `python -m scripts.reference_capture.compare_mesen2 <rom> --frames N` (Mesen2 release binary in `/Applications/Mesen.app`, testRunner mode, `scripts/reference_capture/mesen2_capture.lua`); it owns the flags, isolates battery saves per run and prints Mesen2's `[iNes]`/`[DB]` lines beside NESER's mapper and region, and `scripts/reference_capture/README.md` explains each confound it removes. Name that command in a bead rather than copying flags. The underlying recipe produced a 0-px match against NESER on instr_test-v5 at frame 120 on 2026-09-25.

## Instructions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ specification authority, one or two implementation references, one screenshot re
- The only emulator whose captures approve a NESER golden frame.
- Binary: `/Applications/Mesen.app/Contents/MacOS/Mesen` (official release zip; no
Homebrew cask). Verified recipe, scripts and the `AllowIoOsAccess` toggle:
`scripts/reference_capture/README.md`. In short:
`CAPTURE_FRAME=<n> CAPTURE_OUT=<abs.png> Mesen --testRunner --enableStdout --timeout=30
--Video.VideoFilter=None --Video.AspectRatio=NoStretching --nes.DisableFrameSkipping=true
--nes.RamPowerOnState=AllZeros <rom> scripts/reference_capture/mesen2_capture.lua`.
`scripts/reference_capture/README.md`. Compare with one command, which owns the flags
(frame skip off, zero RAM, a standard pad in both ports) and isolates battery saves per run:
`python -m scripts.reference_capture.compare_mesen2 <rom> --frames <n> --out-dir <dir>`.
- Diff with `python -m scripts.diff_screenshots <neser> <mesen> --shift-search 1`.

## Reporting rules
Expand Down
18 changes: 10 additions & 8 deletions .github/skills/snes-hardware-research/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Every hardware research skill in this repository uses the same three tiers. The
Used only when the specification is missing, incomplete, or ambiguous, to see how a specific behavior can be implemented. Mesen2 (`https://github.com/SourMesen/Mesen2`, `Core/SNES/`) is consulted first, because it is also the project's screenshot reference and a deliberate divergence from it must be commented at the call site; ares (`https://github.com/ares-emulator/ares`, `ares/sfc/`) is the second, independent lineage, consulted when Mesen2 has no model for the behavior or when two implementations agreeing would settle a question. Implementation evidence is never equal authority with the specification. Where an emulator makes a choice the specification does not settle, say so instead of presenting it as hardware fact. bsnes, higan and ares-performance are the same lineage as ares and count as one opinion; Snes9x is not an authority but may break a Mesen2-vs-ares tie (see `references/source-priority.md`).

3. **Screenshot reference: Mesen2** (navigator decision in #3000).
When a visual test ROM needs a reference image, capture it with Mesen2 at the same frame as NESER and pixel-diff the two captures with `python -m scripts.diff_screenshots`. An exact match approves the golden. If the captures differ and Mesen2 itself is suspect, ask the navigator instead of approving either side. ares is never used for screenshots. The headless capture recipe is in "Automating Screenshot Capture at Specific Frames" below; the reusable script is `scripts/reference_capture/mesen2_capture.lua` (see `scripts/reference_capture/README.md`, shared with the NES).
When a visual test ROM needs a reference image, capture it with Mesen2 at the same frame as NESER and pixel-diff the two captures with `python -m scripts.diff_screenshots`. An exact match approves the golden. If the captures differ and Mesen2 itself is suspect, ask the navigator instead of approving either side. ares is never used for screenshots. The headless capture recipe is in "Automating Screenshot Capture at Specific Frames" below; the reusable script is `scripts/reference_capture/mesen2_capture.lua`, and a NESER-against-Mesen2 comparison is one command, `python -m scripts.reference_capture.compare_mesen2 <rom.sfc> --frames N` (see `scripts/reference_capture/README.md`, shared with the NES). Name that command in a bead rather than copying its flags.

## Instructions

Expand Down Expand Up @@ -87,7 +87,7 @@ Every hardware research skill in this repository uses the same three tiers. The
- Capture a Mesen2 screenshot at the same frame as NESER and pixel-diff programmatically; exact matches become the reference for NESER comparison.
- If NESER and Mesen2 disagree and the divergence is suspected to be a Mesen2 quirk, **ask the user** how to proceed rather than approving either side unilaterally.
- Screenshot settings for comparable captures:
- Mesen2: `--Video.VideoFilter=None --Video.AspectRatio=NoStretching --snes.disableFrameSkipping=true --snes.port1.type=SnesController --snes.port2.type=SnesController`
- Mesen2: `--Video.VideoFilter=None --Video.AspectRatio=NoStretching --snes.disableFrameSkipping=true --snes.port1.type=SnesController --snes.port2.type=SnesController` plus `--snes.RamPowerOnState=AllZeros`; `python -m scripts.reference_capture.compare_mesen2` passes all of them
- Mesen2 headless mode: `Mesen --testRunner --enableStdout --timeout=N <rom> <script.lua>`
- **`--snes.disableFrameSkipping=true` is mandatory for animated content** (found in #2990):
headless testRunner emulation runs >100 fps, engaging `_skipRender` (SnesPpu.cpp) which
Expand All @@ -113,11 +113,13 @@ Every hardware research skill in this repository uses the same three tiers. The
- **Plug in the same controllers on BOTH sides** (nr-0an). Mesen2's testRunner takes its
SNES ports from `settings.json`, and a local install may have port 2 empty, while
NESER has a standard pad in each port by default. Pass
`--snes.port1.type=SnesController --snes.port2.type=SnesController` to Mesen2, and pin
NESER's side with `--snes-controller-port1 standard --snes-controller-port2 standard`,
since a `neser.conf` port line (e.g. `multitap`) would otherwise apply. For a game NESER
recognises as a Mouse or Super Scope game, NESER picks that device itself; give Mesen2
the same type (`SnesMouse`, `SuperScope`) instead. Games that read which pads are
`--snes.port1.type=SnesController --snes.port2.type=SnesController` to Mesen2, and keep
a `neser.conf` port line (e.g. `multitap`) from applying on NESER's side.
`python -m scripts.reference_capture.compare_mesen2` does both (it runs NESER with an
empty `--config`). For a game NESER recognises as a Mouse or Super Scope game, NESER
picks that device itself; give Mesen2 the same type instead with
`--mesen2-arg=--snes.port2.type=SnesMouse` (or `SuperScope`), which replaces the pinned
flag for that port. Games that read which pads are
connected play differently otherwise: Super Bomberman 3's attract demo took a lag frame
on Mesen2 only, which looked like a CPU-timing drift until a trace showed a branch on
the game's "player 2 connected" byte going the other way. The lists are
Expand Down Expand Up @@ -385,7 +387,7 @@ When verifying SNES emulator accuracy:
- **CRC-based integration tests**: Capture frame CRCs at known stable points (e.g., frame 600) and use as golden values for regression testing. Update test comments to reference GitHub issues for known differences.
- **Screenshot settings for comparable captures**:
- Mesen2: `--Video.VideoFilter=None --Video.AspectRatio=NoStretching --snes.disableFrameSkipping=true
--snes.port1.type=SnesController --snes.port2.type=SnesController`
--snes.port1.type=SnesController --snes.port2.type=SnesController` plus `--snes.RamPowerOnState=AllZeros`; `python -m scripts.reference_capture.compare_mesen2` passes all of them
(the frame-skip switch is mandatory for animated content; see step 9 of the Instructions;
the port flags match NESER's default controllers, see "Plug in the same controllers")
- Since the BG vertical-scroll display-line fix (issue #2945, PR #2981), NESER and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,10 @@ specification authority, one or two implementation references, one screenshot re

7. **Mesen2** (navigator decision in #3000)
- The only emulator whose captures approve a NESER golden frame.
- Headless test mode: `Mesen --testRunner --enableStdout --timeout=N <rom> <script.lua>`
- Screenshot settings: `--Video.VideoFilter=None --Video.AspectRatio=NoStretching
--snes.disableFrameSkipping=true` (the frame-skip switch is mandatory for animated
content, see SKILL.md), plus `--snes.RamPowerOnState=AllZeros` for any ROM that can
display uninitialised WRAM; pin NESER's side with `--ram-init-mode zero` too.
Always add `--snes.port1.type=SnesController --snes.port2.type=SnesController`, the
standard pads NESER has in each port by default, and pin NESER with
`--snes-controller-port1 standard --snes-controller-port2 standard` (nr-0an).
- Compare with one command, which owns the flags (frame skip off, zero RAM, a standard pad
in both ports, NESER on an empty `--config`) and isolates battery saves per run:
`python -m scripts.reference_capture.compare_mesen2 <rom.sfc> --frames <n> --out-dir <dir>`
(`scripts/reference_capture/README.md` says why each flag is there).
- Capture twice before trusting any non-zero diff; a capture that changes between
identical runs means the reference is not pinned.
- Diff the captures with `python -m scripts.diff_screenshots <neser> <mesen> --shift-search 1`
Expand Down
10 changes: 6 additions & 4 deletions README-SNES.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,10 @@ directory and are never committed. To approve a new or changed golden:
1. Run the test with `NESER_CAPTURE_SCREEN=1` to write a PNG per test under
`target/snes_test_captures/<suite>/` (each suite's source file documents
its specific recording steps).
2. Capture the Mesen2 ground truth for the same ROM/frame (headless
`--testRunner` with a Lua screenshot script). Always pass
2. Capture the Mesen2 ground truth for the same ROM/frame with
`python -m scripts.reference_capture.compare_mesen2 <rom.sfc> --frames <N>`
(see `scripts/reference_capture/README.md`), which passes every flag below
and this step's NESER side; the flags are listed here for why. It passes
`--Video.VideoFilter=None --Video.AspectRatio=NoStretching
--snes.disableFrameSkipping=true` — without the frame-skip switch
Mesen2's testRunner renders only every other frame and screenshots of
Expand All @@ -579,8 +581,8 @@ directory and are never committed. To approve a new or changed golden:
Mesen2 otherwise takes its controller ports from its own settings, which may
leave port 2 empty, and games that check which pads are connected then play
differently from NESER, which has a standard pad in each port (nr-0an).
3. If the ROM can display uninitialised RAM, add
`--snes.RamPowerOnState=AllZeros` to the Mesen2 command line. Mesen2's SNES
3. If the ROM can display uninitialised RAM, Mesen2 needs
`--snes.RamPowerOnState=AllZeros` (`compare_mesen2` passes it). Mesen2's SNES
default is `RamState::Random`, and without the flag the ground truth is not
even self-consistent: for `test_dmatiming/demo.smc` two default Mesen2
captures differ from *each other* by 1.06%, which is what produced #3063's
Expand Down
2 changes: 1 addition & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The `src/bin/roms.rs` file is a library binary (accessed via `cargo run --bin ro
| `scripts/diff_bus_traces.py` | Diffs two SNES CPU bus traces (NESER `--trace-cpu=2` and a reference emulator's) by event **ordinal** rather than by clock, so a constant clock origin difference is ignored and the ordinal where the per-cycle offset *steps* names the exact cycle whose accounting diverges. Auto-aligns the leading edge (a clock window never opens on the same cycle in both emulators) and reports a clock-offset histogram plus side-by-side context. This is what localised #3050 to a single DMA end pad. |
| `scripts/diff_timing_traces.py` | Diffs a NESER timing trace (`timing_trace`) against Mesen2's (`mesen2_nmi_clock.lua`, `mesen2_exec_trace.lua`) by line ordinal: the first pair's clock offset is the baseline, and the first line whose PC or NMI number differs, or whose offset leaves the baseline and stays off it, is the divergence. A one-line excursion is listed as a stamp difference (a stall charged on the other side of an instruction boundary), not failed. Exit 0 match, 1 divergence, 2 no trace lines. |
| `scripts/diff_screenshots.py` | Pixel-diffs two emulator screenshots for the Mesen2 golden-approval workflow. Reports the per-pixel difference the 0-pixel approval rule is written against, an optional `--shift-search N` row/column offset search (a non-zero best shift is evidence of a bug, not a capture convention, so the exit code stays non-zero regardless), and `--rows`, the per-row mean-luminance vector of each image plus the lag that best aligns them -- the diagnostic for scanline-banding ROMs, whose whole signal a 2D shift search cannot express. |
| `scripts/reference_capture/` | Verified headless screenshot recipes for the screenshot-reference emulators (`README.md`), the Lua capture scripts for Mesen2 (`mesen2_capture.lua`, NES and SNES), the Mesen2 timing-trace scripts (`mesen2_nmi_clock.lua`, `mesen2_exec_trace.lua`) with the Lua API behaviours they rely on, and `mgba-headless` (`mgba_capture.lua`), and the one-hunk patch that gives stock `mgba-headless` a framebuffer. Every hardware research skill's Tier 3 points here. |
| `scripts/reference_capture/` | Verified headless screenshot recipes for the screenshot-reference emulators (`README.md`), the NESER-against-Mesen2 comparison command (`compare_mesen2.py`, which owns the Mesen2 flags and isolates battery saves per run), the Lua capture scripts for Mesen2 (`mesen2_capture.lua`, NES and SNES), the Mesen2 timing-trace scripts (`mesen2_nmi_clock.lua`, `mesen2_exec_trace.lua`) with the Lua API behaviours they rely on, and `mgba-headless` (`mgba_capture.lua`), and the one-hunk patch that gives stock `mgba-headless` a framebuffer. Every hardware research skill's Tier 3 points here. |
| `scripts/sort_roms.py` | Sorts ROM files into mapper-numbered subdirectories based on their iNES header. |
| `scripts/prepare_release.py` | Applies a release to the tree for the release skill (`.claude/skills/release/SKILL.md`): computes the next version for a maintenance, minor or major release, sets it in `Cargo.toml` and `Cargo.lock`, replaces the web frontend's idle `SCROLLER_TEXT` sentence with the release date, version and highlights, and copies the navigator-approved notes to `docs/releases/v<version>.md`. `--print-version` only reports the next version. |
| `scripts/package_release.py` | Builds per-target release archives with a top-level `neser/` directory. Includes the binary, runtime resources, README, LICENSE, config example, fonts, and only shader files reachable from configured shader presets. Excludes development scripts. |
Expand Down
27 changes: 27 additions & 0 deletions docs/retrospectives/nr-ocx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# nr-ocx — retrospective

- **Implementer:** Storm
- **Date:** 2026-10-04
- **PR:** #3335

## A rebase brought in a test that imported constants this PR had moved, and only CI saw it

**What happened.** This PR moved the Mesen2 flag lists `NES_FLAGS`/`SNES_FLAGS` out of
`scripts/test_mesen2_capture.py` into `scripts/reference_capture/compare_mesen2.py`. While it was
in review, nr-ggx (#3333) merged `scripts/test_mesen2_traces.py`, which imports those two names
from `test_mesen2_capture`. The rebase applied without a conflict in any Python file. After it I
ran only the two Mesen2 test modules, ruff and mypy, all green. CI's `python-tests` then failed
with `ImportError: cannot import name 'NES_FLAGS' from 'scripts.test_mesen2_capture'`.

**Why.** A semantic conflict: textually separate files, so git had nothing to merge. mypy did not
catch it either, because `scripts/pyproject.toml` sets `ignore_errors = true` for every module
except a short list, and the test modules are not on it.

**Cost.** One CI round (about five minutes) and one of the three fix attempts.

**Prevent by.** After a rebase onto a main that changed anything under `scripts/`, rerun the full
`python -m unittest discover -s scripts -t . -p "test_*.py"` (about a minute) before pushing,
not only the modules the PR touches. A step for that belongs in `produce-bead`'s *Merging*,
where a `422` conflict sends the producer to rebase.

**Seen before.** none found
Loading
Loading