Skip to content

release: v2.2.3 "Datum" — fast dot path promoted, PGO shipped, last two mapper residuals closed#325

Merged
doublegate merged 19 commits into
mainfrom
perf/fast-dotloop-promote-and-expose
Jul 23, 2026
Merged

release: v2.2.3 "Datum" — fast dot path promoted, PGO shipped, last two mapper residuals closed#325
doublegate merged 19 commits into
mainfrom
perf/fast-dotloop-promote-and-expose

Conversation

@doublegate

@doublegate doublegate commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Cuts v2.2.3 "Datum" — a performance-appraisal and accuracy-closure line, plus the version ceremony. 14 commits.

A datum is the fixed reference a measurement is taken from. That is what this line is about: promoting a win that was already proven, pinning the references that keep it honest, and closing the two accuracy residuals that had a real oracle waiting for them.

Performance

  • The specialized PPU fast dot path is now the default — and reachable for the first time. Nes::set_fast_dotloop had zero callers outside the core, so a measured −11.3% frame-time win shipped switched off and unreachable by any user. Now surfaced as an [emulation] config key that defaults on for pre-existing configs. Differential-tested bit-identical every frame (framebuffer, palette-index framebuffer, audio, cycle count, full snapshot) since v2.1.8.
  • PGO binaries reach the release path. pgo.yml already computed a promotion verdict; release.yml never consumed it, so a passing gate still shipped a plain build. Wired, scoped honestly to linux-x86_64 where the gate runs.
  • A same-runner relative frame-time regression gate, closing a hole where a 2.5x slowdown passed the deliberately-loose absolute ceiling.
  • Two optimizations measured and REJECTED, documented with their numbers in docs/performance.md per that file's convention. P3 made the shipped default slower (+4.32% / +3.35%, p ≤ 0.02); P4's remaining lever is capped at ≤1.9%, below the 3% bar.

Accuracy

  • The last two Holy Mapperel residuals are closed — 17/17 now detail=0000 (was 15/17): MMC1's two WRAM write-protect layers and FME-7's open bus on the RAM-selected-but-disabled window, both via the trait's existing cpu_read_unmapped contract.
  • MMC1 is the change Holy Mapperel's README calls a game-compatibility hazard (FCEUX / PowerPak omit it), so it was validated before landing: 60/60 commercial ROMs including seven battery-backed MMC1 saves, plus 138/138 extended.
  • Sunsoft 5B absolute level calibrated against Mesen2, which required widening Mapper::mix_audio to i32 — the correct full-scale tone does not fit i16.
  • A save-state schema gap fixed (PPU_SNAPSHOT_VERSION 8 + an APU v4 tail). This is why AccuracyCoin scored 141/141 headless but 138/141 in the app: run-ahead round-trips PPU state v7 did not carry. A new standing field-vs-schema audit found it and two more gaps mechanically.
  • Zapper beam-relative light model, opt-in / default-off (no pass-fail light-gun ROM exists to adjudicate it).

Housekeeping

  • The eleven sprintN.rs mapper modules (27,631 lines, ~110 boards) renamed for the boards they emulate with mNNN_ prefixes. Verified content-preserving mechanically: 930 top-level items byte-identical, 0 altered, and an identical 172-ID dispatch table.
  • Seven commercial-oracle audio rows, stale since v2.1.6, re-blessed with their provenance — and the structural hole plugged by a CI-visible expansion_level_tripwire, since that suite needs gitignored ROMs and so never runs in CI.

Breaking

  • .rns save states from v2.2.2 and earlier will not load — they fail with a clear version error rather than misinterpreting stale bytes (ADR 0028 / ADR 0034). Movies and netplay are unaffected. Deliberate: the alternative is restoring a broken sprite-evaluation FSM.
  • Mapper::mix_audio returns i32 (was i16). Out-of-tree mappers need the signature updated; every in-tree board returns the values it always did.

Verification

cargo test --workspace --features test-roms         2237 passed / 0 failed / 20 ignored
  AccuracyCoin                                      141/141, incl. through run-ahead
  nestest 0-diff · holy_mapperel 17/17 · pal_apu_tests 10/10 · visual_regression 9/9
external_real_games (commercial-roms)               60/60
external_extended   (commercial-roms)               138/138
fmt · clippy (4 feature combos) · rustdoc · no_std · actionlint · markdownlint   all clean

Release mechanics

On merge, release-auto.yml tags v2.2.3, publishes with .github/release-notes/v2.2.3.md as the body, and invokes release.yml to build and attach the Linux / macOS-aarch64 / Windows binaries. The tag is deliberately not created by hand — doing so would make that job a no-op and skip the binary build.

Mobile versions stay frozen at their v2.0.x host-only release points; the joint store launch remains v2.3.0.

Summary by CodeRabbit

  • New Features
    • RustyNES v2.2.3 “Datum”: promoted the faster PPU dot path to the default.
    • Added opt-in beam-relative Zapper temporal light model.
    • Added opt-in idle-line fast path for the PPU (feature flag), plus a live in-app toggle for the fast dot loop.
  • Bug Fixes / Accuracy
    • Closed remaining mapper accuracy residuals (including MMC1/FME-7) and improved Sunsoft 5B calibration; widened expansion-audio mixing to reduce clipping.
    • Strengthened mid-frame save-state correctness and run-ahead regression coverage.
  • Compatibility
    • Save-state schema updates: PPU v8 and APU v4; older states may not load.

doublegate and others added 14 commits July 22, 2026 22:10
The AccuracyCoin battery measured 141/141 headless but 138/141 in the
desktop frontend. Both numbers were correct; the two paths do not run the
emulator the same way, and the difference exposed a hole in the PPU
save-state schema.

Root cause
----------

`[input] run_ahead` defaults to 1 (`default_run_ahead()`), so every visible
frame the desktop app performs a full core save-state round trip via
`RunAhead::run_frame_ahead` + `finish`:

  1. one persistent `run_frame` (the real timeline),
  2. `Nes::snapshot_core_into`,
  3. N further `run_frame`s (hidden + visible),
  4. `Nes::restore_quiet` back to (2).

Sixty times a second, at an arbitrary point in the frame. The headless
harness drives a straight `run_frame` loop and therefore cannot observe an
incomplete schema at all.

Auditing all 113 fields of `struct Ppu` against everything `snapshot.rs`
writes located the gap. `secondary_oam` -- the BUFFER the sprite-evaluation
pass fills -- has been serialized since v1. The POINTERS AND PHASE that
fill it were not:

  * the per-dot eval FSM: `sprite_eval_read_latch`, `sprite_eval_n` / `_m` /
    `_found` / `_sec_idx`, and the `_copying` / `_done` / `_overflow_search` /
    `_zero_found` / `_first_iter` phase flags;
  * the parallel OAM-data-bus model: `oam_bus_copybuffer`,
    `oam_bus_secondary[32]`, `oam_bus_addr_h` / `_addr_l` / `_secondary_addr`,
    `oam_bus_copy_done`, `oam_bus_sprite_in_range`, `oam_bus_overflow_counter`;
  * `oam2_addr`, the secondary-OAM write pointer maintained across the
    dots 1..=64 clear window.

A checkpoint taken mid-eval therefore restored a fully-populated secondary
OAM alongside a power-on-default walker -- `sprite_eval_read_latch` back to
`0xFF`, `_n`/`_m`/`_sec_idx` back to 0, the copy/overflow phase flags
cleared -- once per frame, forever. The dots 65..=256 pass resumed from the
wrong position in primary OAM against a buffer that claimed a different
number of committed bytes.

Bisected headlessly against the shipped frontend configuration; run-ahead
alone accounts for it (rewind, OAM decay, PPU die revision, power-on RAM
randomization and the palette knobs were all at defaults and all
irrelevant):

  | run-ahead | rewind | result   |
  |-----------|--------|----------|
  | 0         | off    | 141/141  |
  | 0         | on     | 141/141  |
  | 1         | on     | 138/141  |
  | 1         | off    | 138/141  |

reproducing the frontend's failure set and error codes exactly:

  PPU Behavior      :: Rendering Flag Behavior   [error 2]
  Sprite Evaluation :: Arbitrary Sprite zero     [error 2]
  Sprite Evaluation :: Misaligned OAM behavior   [error 1]

This is the third instance of one bug class and the second to reach users.
The v5 tail closed it for the 2-cycle-ALE fetch state (ADR 0030); the v6
tail closed it for the sprite-shifter halt latches and OAM-corruption
arming state, which had produced the Wizards & Warriors half-blank
playfield. Each time the mechanism was identical: live mid-frame PPU state
absent from the schema, invisible to every straight-`run_frame` test,
exposed only by a per-frame snapshot/restore.

Cross-checked against Mesen2, which serializes the equivalent set in
`NesPpu<T>::Serialize` -- `_spriteIndex`, `_spriteCount`, `_sprite0Added`,
`_sprite0Visible`, `_oamCopybuffer`, `_secondaryOamAddr`, `_spriteInRange`,
`_oamCopyDone`, `_overflowBugCounter`. Independent confirmation that this
is live state and not something derivable on load.

Change
------

`PPU_SNAPSHOT_VERSION` 7 -> 8, appending the above as a 50-byte tail
(u8*5 + bool*5 eval FSM; u8 + [u8;32] + u8*3 + bool*2 + u8 OAM-data-bus
model; u8 `oam2_addr`). Writer and reader are field-order symmetric and
pinned by a round-trip unit test. `Ppu::restore` accepts 1..=8 and
upconverts pre-v8 blobs to the constructor defaults (`0xFF` read latches,
`[0xFF; 32]` parallel secondary OAM, `0`/`false` elsewhere) -- the at-rest
state, and precisely what a pre-v8 restore left behind -- for direct
callers that bypass the version-exact container.

Separately, the scanline-classification cache (`cached_visible`,
`cached_pre_render`, `cached_render_line`, keyed by `flags_cached_scanline`)
is now INVALIDATED on restore at every schema version rather than
serialized. It is a pure function of `scanline` + `region`, both already in
the blob, so recomputation is equivalent and costs no bytes -- while a warm
key carried across a restore could satisfy the fast dot path's
`scanline == flags_cached_scanline` staleness guard against a value
computed under a different timeline. Mesen2 makes the same call, recomputing
its derived state in the `if(!s.IsSaving())` post-load block.

Blob growth is 50 bytes, ~0.02% against the 245,760-byte framebuffer that
dominates it -- no measurable cost to run-ahead, rewind, or rollback.
Nothing outside `snapshot`/`restore` is touched, so a plain `run_frame`
timeline is byte-identical by construction: every golden vector, nestest,
`pal_apu_tests`, the visual-regression corpus and the headless battery are
unmoved.

Regression net
--------------

`crates/rustynes-test-harness/tests/accuracycoin_runahead.rs` reruns the
entire battery through the run-ahead cycle at depths 1 and 2 and asserts no
test is lost, naming any that is. This closes the CLASS rather than the
instance: `runahead.rs`'s existing regressions compare framebuffers on two
ROMs, a weaker oracle than a battery that probes the eval FSM at dot
resolution and reports which behavior broke. It reimplements the cycle
rather than importing `RunAhead` because `rustynes-frontend` pulls in
wgpu/winit/cpal, which the harness crate must not depend on.

Netplay rollback and TAS seeking take the same in-process round trip and
inherit the fix.

Verification
------------

  cargo test --workspace --features test-roms   2209 passed, 0 failed, 20 ignored
  AccuracyCoin headless                          141/141 (unchanged)
  AccuracyCoin, run-ahead depth 1 and 2          141/141 (was 138/141)
  cargo fmt --all --check                        clean
  cargo clippy --workspace --all-targets -D warnings          clean
  clippy: scripting / scripting,hd-pack / retroachievements / test-roms   clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features   clean
  pre-commit run --files <changed>               all hooks passed

Docs
----

ADR 0034 records the decision and the accepted compatibility break.
`docs/ppu-2c02.md` gains a save-state-coverage subsection under sprite
evaluation plus the derived-cache note under the fast dot path.
`docs/STATUS.md`'s AccuracyCoin row records that the battery now also runs
under run-ahead. CHANGELOG `[Unreleased]` carries `### Fixed` and
`### Changed` entries.

BREAKING CHANGE: existing `.rns` save states will not load. The container is
version-exact per section (`bus.rs`: `s.version != PPU_SNAPSHOT_VERSION` ->
`SnapshotError::VersionMismatch`), so unlike the additive v3-v7 tails this
one fails pre-v8 states closed. Taken deliberately: a pre-v8 state genuinely
does not contain the eval bytes, so "loading" it means resuming with a reset
walker beside a populated buffer -- the exact defect this closes -- and a
migration would have to invent the missing state. Failing closed with a
named error is the honest report, the precedent ADR 0028 set. Movies
(`.rnm`) and netplay are unaffected; both re-derive from a fresh power-on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the mechanical diff that found the v8 sprite-evaluation gap into a
standing test, so the bug class stops depending on someone remembering the
technique after a user reports a symptom.

Why
---

The same defect has now shipped three times:

  v5 (ADR 0030)  2-cycle-ALE in-flight fetch state       netplay-rollback desync
  v6             sprite-shifter halt, OAM-corruption arm  W&W half-blank playfield
  v8 (ADR 0034)  sprite-evaluation FSM, OAM-data-bus     AccuracyCoin 141 vs 138

Every instance is the same shape: a live mid-frame field is added to a chip
struct and the matching `snapshot.rs` is not updated. Nothing catches it,
because NO straight-`run_frame` test can -- a forward run never round-trips
its own state, so an incomplete schema is invisible until something
snapshots and restores mid-frame. Three things do: the frontend's run-ahead
(once per visible frame, on by default), netplay rollback, and TAS seeking.
All three were found by hand, after the symptom.

The check itself is trivial and would have caught all three at the commit
that introduced them, in milliseconds, with no ROM and no emulation.

What it does
------------

Chip source and serializer are embedded with `include_str!`, so the audit is
compile-time hermetic: no filesystem access, no working-directory
dependence, and it runs in the default `cargo test` job (no feature gate).
For every field of `Ppu` / `Cpu` / `Apu`, the body of that chip's
`pub fn snapshot(&self) -> Vec<u8>` must reference `self.<field>` at a word
boundary, or the field must appear on an exclusion list with a written
reason.

Two properties carry the whole thing, and both are pinned by their own
tests:

  * SCOPED TO THE WRITER, not the file. A field appears all over `restore`,
    most visibly in the pre-version upconvert branches that assign it a
    default (`self.sprite_eval_n = 0;`). Those assignments are exactly what
    an unserialized field looks like, so a whole-file search reports the bug
    as its own fix. Confirmed by negative control: removing `sprite_eval_n`
    from the writer does NOT trip a file-wide search, and DOES trip this
    one. (The `W`/`R` helpers and the APU's `write_*` functions take
    `&Pulse` / `&Dmc` / ... rather than `&self`, so writer scoping loses no
    coverage on any of the three chips.)

  * WORD-BOUNDARY MATCHING. A plain substring search lets `self.oam_addr`
    satisfy a field named `oam`, which would hand every short name a free
    pass.

Four further tests guard the lists themselves, so the audit cannot rot into
a rubber stamp: exclusion entries must name real fields; an exclusion entry
whose field IS serialized is rejected as stale; the known-gap set is
asserted exactly; and the nineteen v8 sprite-evaluation fields are pinned by
name so a reviewer sees WHICH bug reopened rather than an anonymous count.

The failure message is deliberately opinionated -- it tells the reader the
default answer is to serialize the field behind a version bump, not to
allowlist it, because that has been correct three times out of three.

Scope: coverage, not correctness. It proves the writer touches each field,
not that the writer and reader agree; field-order symmetry stays pinned by
the per-tail round-trip unit tests in each `snapshot.rs`.

What it found
-------------

Writer scoping immediately surfaced five fields the weaker whole-file
version had masked, all of them assigned in `restore` and never written:

  * PPU `cached_visible` / `cached_pre_render` / `cached_render_line` /
    `flags_cached_scanline` -- genuinely derived (a pure function of
    `scanline` + `region`, both serialized) and invalidated on restore per
    ADR 0034. Listed with that reason.

  * APU `restored_parity_tail` -- restore-produced protocol metadata
    reporting whether the loaded blob carried the Stage-4 parity/DMA tail,
    consumed by the bus immediately afterward. Serializing it would be
    circular. Listed with that reason.

And two genuine, previously unrecorded gaps, now pinned in `known_gaps`
rather than quietly misfiled as derived:

  * APU `reset_4017_delay` / `reset_4017_value` -- the scheduled warm-reset
    `$4017` re-write (v2.0.0 beta.3 A4). A snapshot taken in the few CPU
    cycles between `Apu::reset` arming the countdown and
    `tick_with_external` consuming it restores 0 and drops the re-write.
    Narrow window and no known symptom, but it is the same class as the
    v5/v6/v8 PPU tails and wants an `APU_SNAPSHOT_VERSION` tail of its own.
    NOT fixed here -- that is a separate change with its own version bump.
    `known_gaps` is asserted exactly, so closing it (or opening another)
    fails this test until the list is updated.

Verification
------------

  cargo test -p rustynes-test-harness --test snapshot_schema_audit   6 passed
  negative control (drop `sprite_eval_n` from the writer)            2 tests fail, correctly named
  cargo fmt --all --check                                            clean
  cargo clippy --workspace --all-targets -D warnings                 clean
  cargo test -p rustynes-ppu --lib snapshot                          14 passed
  pre-commit run --files <changed>                                   all hooks passed

No emulation code is touched; this commit adds a test and a CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the two gaps the standing schema audit surfaced in the previous
commit -- the first instance of this bug class found by tooling rather than
by a user reporting a symptom.

The defect
----------

`Apu::reset` does not re-write `$4017` immediately. Per the blargg
`apu_reset` spec it SCHEDULES one: `reset_4017_value` takes the retained
mode bit and `reset_4017_delay` is armed at 2, then `tick_with_external`
decrements the countdown once per CPU cycle and calls
`FrameCounter::write` when it reaches zero. The 2-cycle placement is
empirically calibrated -- blargg `4017_timing` measures 8 against an
accept window of 6..=12 (v2.0.0 beta.3 A4).

Neither field was serialized. A snapshot taken inside that 2-cycle window
restored `delay = 0`, which does not mean "already fired" -- it means
"nothing scheduled". The re-write was silently cancelled, and the restored
frame counter carried on with the sequencer phase the re-write exists to
reset.

Same class as the PPU's v5 / v6 / v8 tails (ADR 0030 / ADR 0034): live
state absent from the schema, invisible to any straight-`run_frame` test
because a forward run never round-trips itself, and reachable only through
snapshot/restore -- run-ahead, netplay rollback, TAS seek.

Reachability is genuinely narrow and no symptom was ever attributed to it:
the window is 2 CPU cycles, and the round trips that matter checkpoint on
frame boundaries, so a reset must land essentially exactly there. Fixed
anyway -- the cost is two bytes, and "narrow" is not "impossible".

Change
------

`APU_SNAPSHOT_VERSION` 3 -> 4, appending both fields (2 bytes). v1..=3
blobs upconvert to `0`/`0` -- "no re-write pending", the resting value and
therefore correct for any pre-v4 state not captured inside the arming
window (and for one that was, the bytes do not exist to recover).

DEPARTS FROM THIS MODULE'S CONVENTION, deliberately. The earlier tails
here -- the v1.x DMC-DMA scheduling bytes and the W3-Stage-4 block -- are
trailing-optional, detected with `has_remaining` and added without a
version bump. That works but leaves two different blob lengths both valid
at one version; a version gate does not, and a short v4 blob now reports
`Truncated` rather than being quietly accepted. The bump costs no
additional compatibility because the same change already takes
`PPU_SNAPSHOT_VERSION` to 8 and `rustynes_core`'s `.rns` container is
version-exact per section, so pre-existing save states are already
rejected at the PPU section. The version-acceptance check also collapses
to `matches!(version, 1..=APU_SNAPSHOT_VERSION)` so it cannot go stale on
the next bump.

Tests
-----

Three, and the negative control materially changed the third:

  * `v4_round_trips_the_scheduled_reset_4017_rewrite` -- field round trip.
  * `pre_v4_blob_upconverts_reset_4017_to_no_pending_rewrite` -- a v3 blob
    restores as "nothing scheduled", over a receiver deliberately holding
    non-zero values so a missing assignment cannot pass.
  * `a_reset_survives_a_snapshot_restore_taken_mid_countdown` -- the
    behavioural pin. THE FIRST VERSION OF THIS TEST WAS VACUOUS: it passed
    with the tail removed. Two independent reasons, both worth recording
    because either alone makes the test worthless:

      - `frame_counter.mode` cannot discriminate. `reset_rewrite_4017`
        retains bit 7, so the scheduled re-write always restores the mode
        already in effect and the field reads identically either way.
      - four ticks was too few. The countdown fires at t=2, and
        `FrameCounter::write` then schedules its OWN 3/4-cycle maturation
        before the sequencer restarts, so at t=4 both sides still agree.

    Rewritten to advance 10 cycles and compare `frame_counter.cycle`, it
    fails without the tail with `left: 10, right: 5` -- the restored
    sequencer counting straight through instead of restarting. Confirmed
    by re-running the control after the rewrite.

Audit
-----

`known_gaps` in `snapshot_schema_audit.rs` is now EMPTY, and the comment
there says why that matters: its only two entries were these, found by the
audit and closed here. Every field of `Ppu` / `Cpu` / `Apu` is now either
serialized or listed as derived/config with a written reason. The list is
still asserted exactly, so a future entry is a deliberate documented
admission rather than a default.

Verification
------------

  cargo test --workspace --features test-roms   2218 passed, 0 failed, 20 ignored
  cargo test -p rustynes-apu --lib snapshot     11 passed
  negative control (drop the v4 read)           2 tests fail, incl. the behavioural one
  AccuracyCoin headless                          141/141 (unchanged)
  AccuracyCoin, run-ahead depth 1 and 2          141/141 (unchanged)
  pal_apu_tests                                  10/10
  cargo fmt --all --check                        clean
  cargo clippy --workspace --all-targets -D warnings          clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features   clean
  pre-commit run --files <changed>               all hooks passed

`docs/apu-2a03.md`'s reset section gains the save-state-coverage note,
including the two reasons `mode` is the wrong oracle and `cycle` the right
one, so the next person writing a test here does not rediscover it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Ppu::tick` is the emulator's hottest function. v2.1.8 A1 added
`tick_visible_render_fast` -- a straight-line handler for the common
undisturbed visible background dot -- proved it byte-identical, measured it
at +12.3%, and then shipped it OFF, deferring promotion to "maintainer
review and a clean-host Criterion confirmation".

That confirmation is now in hand, so it defaults on. Two things this pass
established that make the deferral no longer tenable:

1. The knob was UNREACHABLE. `Nes::set_fast_dotloop` had zero callers
   outside `rustynes-core` and its tests -- nothing in `rustynes-frontend`,
   `-libretro`, or `-mobile`. There was no configuration of any shipped
   binary that could turn it on. The optimization existed, was proven, and
   was inert.

2. A fresh profile puts `Ppu::tick` at 32.8% of frame self-time over the
   committed 7-ROM training corpus -- still the single dominant hot
   function, so this is still the right target.

Measurement
-----------

Clean-host Criterion (`cargo bench -p rustynes-core --bench full_frame`, no
concurrent build load -- the contamination that forced A1's interleaved
harness):

  nes_run_frame_nestest           4.4343 ms -> 3.9331 ms   -11.3%
  nes_run_frame_flowing_palette   2.6741 ms -> 2.6723 ms   -0.07% (noise)

The rendering-disabled demo is unchanged because its guard bails at
`rendering_enabled()`; real games render nearly all the time, so -11.3% is
the representative figure. This reproduces A1's +12.3% by a different
method, and clears the standing >3%-and-byte-identical bar.

Byte-identity
-------------

Not newly asserted -- held continuously since v2.1.8 by
`fast_dotloop_diff.rs`, which runs both paths over the corpus and compares
framebuffer + palette-index framebuffer + audio + CPU-cycle count + full
core snapshot EVERY FRAME. The fast path was never unproven, only unshipped.

Re-verified with the new default across the whole `--features test-roms`
suite: 2219 passed / 0 failed. That is the pre-flip 2218 plus exactly the
one config test added here -- no test changed its verdict because of the
promotion. AccuracyCoin 141/141, nestest 0-diff, `visual_regression` and the
APU oracles unmoved.

Promotion changes the default, not the behaviour: the frame the fast path
produces is the frame the exact path produces, by construction and by test.
`false` still selects the fully-general per-dot path, which remains the
always-correct fallback and still runs for every dot the guard does not
admit (~31% of them).

User surface
------------

Desktop gains `[emulation] fast_dotloop` (Settings -> Accuracy, labelled
"performance, not accuracy"), pushed into the core through the existing
`apply_ppu_hardware_config` path.

It is defaulted through `default_fast_dotloop()` rather than
`#[serde(default)]`, which matters more than it looks: `bool`'s serde
default is `false`, so the obvious spelling would have silently opted every
existing user OUT of an ~11% speedup on upgrade -- invisibly, because both
paths render identically. `EmulationConfig`'s `#[derive(Default)]` is
replaced with a hand-written impl for the same reason (a derived `Default`
cannot express one true field among five false ones, and would have
disagreed with the core). Pinned by
`emulation_fast_dotloop_defaults_on_for_pre_v2_2_3_configs`, which covers
all four shapes: a pre-key `[emulation]` section, no section at all, an
explicit opt-out surviving a round trip, and `Default` agreeing with serde.

`rustynes-libretro` and `rustynes-mobile` deliberately gain NO option. Both
already inherit the win from the core default, and neither exposes any
comparable knob today (libretro's `CoreOptions` impl is empty) -- adding
each crate's first-ever config surface, plus UniFFI regeneration and
platform UI, is not justified for an escape hatch with no accuracy
rationale. Called out here rather than left as a silent gap.

Docs
----

`docs/performance.md` §A1 gains a v2.2.3 decision block with the
confirmation table, and records that the win was unreachable before now.
`docs/ppu-2c02.md`'s fast-dot-path section had three stale claims (heading
"opt-in, default-OFF", "default-OFF runtime knob", and "Exact path (the
shipped default)") -- all corrected, with the exact path now described as
the fallback that still serves the ~31% of dots outside the guard.

Verification
------------

  cargo test --workspace --features test-roms   2219 passed, 0 failed, 20 ignored
  cargo test -p rustynes-test-harness --features test-roms --test fast_dotloop_diff   3 passed
  cargo test -p rustynes-frontend --lib          463 passed
  cargo test -p rustynes-test-harness --test snapshot_schema_audit   6 passed
  cargo fmt --all --check                        clean
  cargo clippy --workspace --all-targets -D warnings          clean
  clippy: scripting / scripting,hd-pack / retroachievements   clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features   clean
  pre-commit run --files <changed>               all hooks passed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… bar)

P2 of the performance pass: extend the fast dot path to the ~31% of dots A1
does not cover, starting with the cheapest slice to prove -- the IDLE LINE
(post-render 240 plus vblank 242..=260, 6,820 of the 89,342 NTSC dots).

Implemented, proven byte-identical, and shipped OFF: it does not clear the
project's >3% adoption bar. Retained behind the `ppu-idle-line-fast` cargo
feature rather than deleted, on the same principle as the A2 SIMD blitter --
a measured negative is a result worth keeping, and this one becomes
worthwhile if per-dot dispatch ever gets cheaper.

Why it looked promising
-----------------------

On an idle line the general per-dot body provably reduces to three
assignments -- every other branch is gated on `render_line`, `visible`,
`pre_render`, `scanline == vblank_start_line()`, or a disturbance countdown.
So ~30 predicates were being evaluated to perform three stores. Auditing
that reduction (statement by statement through all 578 lines) is what the
work actually consisted of.

Implementation
--------------

`Ppu::tick_idle_line_fast`, dispatched behind a guard requiring a warm
scanline-classification cache (new derived `cached_idle_line`, keyed by the
existing `flags_cached_scanline`) and all three sub-dot countdowns idle.
Byte-identical by construction on A1's terms: same assignments, same order,
from the same single `mask.rendering_enabled()` read.

`$2006` (`copy_v_delay`) and `$2001` (`mask_write_delay`) are load-bearing
guard conditions -- both are legal during vblank and that is when most games
issue them. `ppudata_sm_countdown` is belt-and-braces: it is armed only under
`rendering_enabled() && is_render_scanline()`, so it cannot currently be live
on an idle line. Tested anyway so the guard is self-evident from itself
rather than resting on an invariant enforced 300 lines away.

Measurement
-----------

Same-session Criterion A/B, feature-off vs feature-on, against a +-0.7% noise
floor (established by re-running an IDENTICAL configuration against its own
baseline -- all four workloads p > 0.05):

  nes_run_frame_nestest              +0.16%   p=0.23, no change
  nes_run_frame_nestest_fast         +0.41%   p=0.01, marginally worse
  nes_run_frame_flowing_palette      -1.31%
  nes_run_frame_flowing_palette_fast -1.55%

A ~1.5% win only on rendering-DISABLED content, neutral-to-slightly-negative
on the rendering-heavy case that dominates real play. The guard runs on every
dot A1's guard does not already claim (~28k/frame, and all 89k when rendering
is off -- which is exactly why the rendering-disabled demo is where it pays)
to save work on 6,820 idle dots whose general path was already
short-circuiting on a cached bool. The two roughly cancel.

Compile-time and not a runtime knob PRECISELY because the cost is the per-dot
guard: a runtime flag would still pay it when disabled. With the feature off
the field, the guard and the handler are all absent, so the default build is
unchanged -- confirmed against the pre-P2 numbers (nestest_fast 3.9331 ->
3.9359 ms, flowing_palette_fast 2.6723 -> 2.6730 ms, both within 0.1%).

Test coverage
-------------

`fast_dotloop_diff` gains `idle_line_fast_path_matches_exact_under_vblank_io`,
which drives a purpose-built NROM (built inline; 6502 source in the doc
comment) that hammers `$2000`/`$2001`/`$2006`/`$2007` for the length of vblank.
The corpus already covered idle dots incidentally -- it hashes whole frames --
but NOT the case that matters: a register write landing ON an idle line, where
the guard must fall through. This makes that coverage structural instead of
accidental.

Two things the negative controls taught, both recorded in-tree
--------------------------------------------------------------

1. THE FIRST A/B WAS CONTAMINATED AND NEARLY GOT THIS DELETED. It reported a
   +2% to +7.3% regression. The "off" baseline had been produced by
   short-circuiting the guard with `if false && ...` while leaving
   `cached_idle_line` in the struct -- the field changed `Ppu`'s layout, and
   the layout, not the guard, moved `flowing_palette` by ~3%. Only a genuine
   feature-off build compares like with like. When A/B-ing a change that adds
   a struct field, the baseline must not carry the field.

2. THE THREE ASSIGNMENTS ARE PROVABLY DEAD given the guard -- deleting any one
   leaves the entire differential suite green (verified twice). The mask
   cannot change without a `$2001` write, which arms `mask_write_delay` and
   routes those dots through the general path. They are kept anyway: "same
   assignments, same order" is checkable by reading twenty lines, whereas
   "these stores are dead" is a reachability argument that must be re-derived
   whenever the guard moves. The corollary matters for future work -- a
   negative control that deletes a dead store proves nothing. The control that
   DOES discriminate breaks the classification: treating line 241 as idle
   fails all four differential tests, including the new torture case.

Verification
------------

  cargo test --workspace --features test-roms   2220 passed, 0 failed, 20 ignored
      (= P1's 2219 plus exactly the one torture test added here)
  cargo test -p ...test-harness --features test-roms,ppu-idle-line-fast \
      --test fast_dotloop_diff                                      4 passed
  cargo test -p ...test-harness --features test-roms --test fast_dotloop_diff  4 passed
  cargo test -p rustynes-ppu --lib          91 passed (both feature configs)
  cargo test -p ...test-harness --test snapshot_schema_audit         6 passed
  cargo fmt --all --check                                            clean
  cargo clippy --workspace --all-targets -D warnings                 clean
  cargo clippy -p ...test-harness --features test-roms,ppu-idle-line-fast   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features  clean

The schema audit flagged `cached_idle_line` the moment it was added, exactly
as designed; it is listed as derived (recomputed from `scanline` + `region`,
invalidated on restore) alongside the other three `cached_*` flags.

Docs: `docs/performance.md` gains a §P2 section carrying the full measurement
INCLUDING the contaminated first A/B (this file's convention is to record
optimizations that did not clear the bar, not just the ones that did --
cf. the A2 SIMD blitter); `docs/ppu-2c02.md` notes the gated handler in the
fast-dot-path section; `docs/STATUS.md` gains the feature-flag table row;
CHANGELOG under Added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `PGO` workflow has computed a profile-guided-optimized build behind a
>3%-faster AND byte-identical gate since v1.2.0. Nothing ever consumed the
result: it ran on the release tag, promoted an artifact, uploaded it, and
`release.yml` attached the plain build regardless. The gate worked, the
measurement was real, and the win reached exactly zero users.

`release.yml` now calls the PGO workflow and replaces the
`x86_64-unknown-linux-gnu` asset with the promoted binary.

Wiring
------

`pgo.yml` gains a `workflow_call` trigger exposing `promotable`,
`speedup_pct` and `artifact` (the artifact NAME is published only when the
gate promoted, so the caller's `if:` and its `download-artifact` name agree
by construction rather than by two places staying in sync).

`release.yml` gains two jobs: `pgo` (calls the workflow) and
`pgo-linux-asset` (downloads the promoted binary, repackages it into the
identical archive layout, and `gh release upload --clobber`s it over the
plain asset under the same name, so download links keep their shape).

The workflow's own `push: tags` trigger was REMOVED. It exists to be called
now, and keeping both would start a second redundant 90-minute run for every
hand-pushed tag. (The auto-release path was never affected either way:
release-auto pushes tags with GITHUB_TOKEN, which does not re-trigger
`on: push` — which is also precisely why release.yml has to CALL this
workflow rather than rely on a tag-push run existing to borrow an artifact
from.)

Scope: linux-x86_64 only. PGO training must RUN the instrumented binary, so
each further target needs its own native runner doing a full ~90-minute
train cycle. macOS-aarch64 and Windows keep shipping plain release builds;
extending PGO to them is a separate decision with a real cost, not a
freebie.

Failure semantics
-----------------

A GATE verdict -- slower than 3%, or an oracle divergence -- is not a
failure. The determinism step now carries a step-level `continue-on-error`,
so it produces `promotable=false` instead of killing the job; the gate step
below it already read `oracle_ok` and treated unset as FAIL, so this is the
behaviour the workflow header has always claimed ("a failure of either is
informational") and never actually had.

An INFRASTRUCTURE failure still reddens the run, deliberately -- a broken
PGO pipeline should be visible. The RELEASE is correct either way: `build`
has already attached plain assets for all three targets and the replacement
job is skipped.

Sequencing: `build` attaches the plain Linux archive in ~10 min; PGO takes
up to 90. The release is therefore complete and downloadable immediately and
then upgraded in place. Withholding the whole release for an hour and a half
to avoid an in-place swap is the worse trade.

actionlint caught a fatal error I had shipped
---------------------------------------------

`continue-on-error` is NOT permitted on a reusable-workflow `uses:` job --
GitHub allows only name/uses/with/secrets/needs/if/permissions there. My
first version put it on the caller, i.e. the entire "never blocks a release"
guarantee rested on a key GitHub would reject. actionlint flagged it as a
syntax error; the tolerance moved inside pgo.yml where it is legal.

Worth noting for anyone reading the tool's output: the three accompanying
"property not defined in object type {}" complaints about
`needs.pgo.outputs.*` were NOT false positives -- actionlint declines to
resolve outputs for a job carrying an invalid key. They vanished the moment
the real error was fixed.

Two latent bugs fixed in passing, both of which this wiring would have hit:

  * `github.event.inputs.frames` is empty on `workflow_call` (it is populated
    only for workflow_dispatch), so the caller's value would have been
    silently dropped and every release would have trained on the default.
    Now `inputs.frames`, which covers both triggers, bound via `env:`.
  * The BOLT job's condition admitted any non-dispatch event, so once
    release.yml began calling this workflow it would have fired on EVERY
    release -- ~90 additional minutes producing an artifact nothing consumes
    (the release ships the PGO binary, not the BOLT one). Now explicit
    workflow_dispatch + run_bolt only.

Pre-existing actionlint findings also fixed
-------------------------------------------

The repo now lints clean end to end, which is what made the above catchable:

  * `ios.yml` picked an Xcode 26 toolchain with `ls -d … | sort | tail -1`
    (SC2012). Replaced with shell globbing. The ordering here is load-bearing
    and subtle -- `Xcode_26.app` outranks `Xcode_26.<n>.app` because after the
    shared `Xcode_26.` prefix `a` (0x61) sorts above any digit, so the
    canonical app is the lexically-HIGHEST name, not the first. Rather than
    assume the rewrite was equivalent, it was differential-tested against six
    synthetic runner layouts (canonical only; canonical + point releases;
    point releases only; none present; unrelated versions only; and the
    26.9/26.10 case that shows the pre-existing lexical bound the file's own
    comment already documents). All six identical.
  * The `agy` self-hosted runner label was reported unknown. Declared in a new
    `.github/actionlint.yaml` -- the mechanism actionlint's diagnostic points
    at. A hosted label is not an option: that runner holds the `agy` CLI's
    OAuth session, which is the whole reason the reviewer is self-hosted.

Verification
------------

  actionlint                                     0 findings, whole repo
  python3 -c yaml.safe_load  (both workflows)    parse OK
  cross-checked every `needs.pgo.outputs.*` consumed against the
      `workflow_call` outputs declared, and each against its backing
      job-level output                           consistent
  bash differential test of the SC2012 rewrite   6/6 cases identical
  pre-commit run --files <changed>               all hooks passed

No Rust source is touched, so the emulation core, its oracles and every
golden vector are unaffected by construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `bench` job has protected an ABSOLUTE ceiling since v1.6.0: headless
frame production must stay under 10 ms, 60% of the 16.67 ms NTSC deadline.
That answers "is the emulator still real-time?" -- not "did this change make
it worse". At the ~4 ms/frame the core actually runs at, a change could get
2.5x SLOWER and still pass the gate.

That is a real hole, not a hypothetical. This repo's own history contains a
10% frame-time swing (the v2.1.8 fast dot path, promoted to default two
commits ago) that the ceiling would not have noticed in either direction.

Adds `scripts/bench_relative_check.sh` alongside -- not replacing -- the
ceiling. It builds and benches the base commit and HEAD BACK TO BACK ON THE
SAME RUNNER, in one job sharing one target dir, and fails if HEAD is more
than `BENCH_MAX_REGRESSION_PCT` (default 10%) slower.

Why a percentage gate is sound now
----------------------------------

v1.6.0 judged one too flaky, and its reasoning is preserved verbatim in the
older script because it was RIGHT -- about cross-run comparison. Comparing
this run's number against a figure recorded on another machine does drown in
the tens-of-percent spread between hosted runners.

This gate never does that. Two builds, measured back to back, on one runner:
runner-to-runner variance is common-mode and cancels. That is the identical
technique `pgo.yml` has relied on since v1.2.0 for its >3% promotion bar, and
the back-to-back noise floor measured during this performance pass is +-0.7%
(docs/performance.md P2 -- an identical configuration benched against its own
baseline reported "no change" on all four workloads, p > 0.05).

The 10% default sits far above that deliberately. A CI runner is noisier than
a quiet desktop, and this gate exists to catch the gross regression the
ceiling sleeps through, not to adjudicate a 2% micro-optimization.
Tightening it is a decision to make once there is CI noise data to justify
it, not up front.

Design
------

The base commit is benched in a THROWAWAY GIT WORKTREE, never via
`git checkout`: a gate must not be able to disturb the working tree it was
invoked from, even if it dies mid-run (cleanup is on an EXIT trap). Both
benches share one `CARGO_TARGET_DIR`, which is what makes the comparison
same-runner, lets criterion see the saved baseline across the two checkouts,
and shares compiled dependencies so the base build is not cold.

It SKIPS with exit 0 -- rather than manufacturing a verdict -- whenever no
base commit is resolvable: shallow clone, root commit, a brand-new branch
whose `github.event.before` is all-zeros, or a `workflow_dispatch` with no
base at all. The `bench` job therefore now checks out with `fetch-depth: 0`,
without which the gate would silently skip on EVERY run and look green while
testing nothing.

Comparison reads the two saved criterion baselines directly rather than
parsing `change/estimates.json`, which is only written when a `--baseline`
comparison ran; doing the arithmetic in-script also keeps it visible in the
log.

Verified
--------

  syntax (bash -n)                                    OK
  shellcheck (both bench scripts)                     clean
  actionlint (whole repo)                             clean
  skip path: base == HEAD                             exit 0, no bench run
  skip path: unresolvable ref                         exit 0, no bench run
  end-to-end HEAD~1 vs HEAD                           exit 0, gate passed
  negative control (threshold -99%)                   exit 1, both benches
                                                      flagged, guidance printed
  worktree cleanup + working tree untouched           confirmed after both runs

The end-to-end run is itself corroborating evidence for the method: HEAD and
HEAD~1 differ only in CI YAML -- no Rust -- and the gate measured -0.73% and
+0.75%. That is the noise floor, on the real hardware, through the real
script.

Security review of the CI wiring (the job now builds two commits):
`ci.yml` triggers on `pull_request`, NOT `pull_request_target`; workflow-level
permissions are `contents: read`; the checkout keeps
`persist-credentials: false`. The job already built and ran PR code, and the
commit newly added to that is the BASE -- trusted history from the target
repo -- so there is no new exposure. `BENCH_BASE_REF` is bound through `env:`
rather than interpolated into the command, and the script validates it with
`git rev-parse --verify` before use, passing only the resolved SHA to
`git worktree add`.

No Rust source is touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 5B's logarithmic DAC *shape* has been hardware-exact since v2.1.6, but
its *absolute* level was a documented gap in the accuracy ledger. The
blocker was never the value -- it was the TYPE. `Mapper::mix_audio` returned
`i16`, and the correct full-scale 5B tone is `1882 * 18.471 = 34,761`, past
`i16::MAX` for a SINGLE channel; three simultaneous full-volume tones
(Gimmick!, Hebereke) reach ~104 k, 3.2x over. The level could not be
corrected while the return type could not hold it.

Widening the trait return to `i32` unblocks it.

Deriving the target
-------------------

From Mesen2 source (the project's stated accuracy bar) rather than from our
own prior numbers, using the same method the VRC6 calibration used. In
`NesSoundMixer::GetOutputVolume` a full-volume 2A03 square is
`(95.88 * 5000) / (8128/15 + 100) = 746.9` units, and the 5B is summed at
weight `* 15` over `Sunsoft5bAudio::_volumeLut = (uint8_t)1.1885^(2i)`:

  volume 12: 63 * 15 / 746.9 = 1.265x   <- what db_5b compares
  volume 15: 177 * 15 / 746.9 = 3.554x  <- full scale, the i16 blocker

That independently reproduces the ~1.27x / ~3.56x figures the ledger
recorded when the work was deferred. The upstream ROM README confirms the
comparison point ("db_5b - Full volume APU square vs. volume 12 5B square").

The in-repo technical references (`ref-docs/nesdev-wiki-technical-report.md`,
the Mesen2 tech report, the architecture design doc) describe the chip but
pin NO absolute level -- expected, since expansion levels are a mixer
convention rather than a hardware spec. That is precisely why the reference
emulator is the oracle here. Checked rather than assumed.

The scale itself is measured, not computed: `db_5b` read `0.0685x` before
(~23 dB too quiet), so `1.2652 / 0.0685 = 18.471`, expressed as the rational
`SUNSOFT5B_MIX_SCALE_NUM/DEN = 2549/138`. Measured `1.2651x` after. Same
measure-then-fix method `NAMCO163_MIX_SCALE` used for its ~12 dB correction.

Shape and level are now separate constants with separate oracles: the shape
by `sunsoft5b_volume_dac_follows_logarithmic_step_law` (a unit test on the
step ratios), the level by the new `level_db_5b` (the `db_5b` ROM, 1.265
+- 0.04). `SUNSOFT5B_LOG_VOL` is unchanged.

Every other board returns exactly the values it always did -- the widening
is representational. Verified before touching the level: 696 mapper unit
tests and all 24 pre-existing audio-expansion tests passed unchanged with
only the type widened.

The NSF path had to follow
--------------------------

`NsfExpansion::mix` summed the chips into an `i16` WITH A CLAMP. Harmless
while every chip fitted -- but a calibrated 5B reaches ~104 k, so an NSF 5B
tune would have CLIPPED where the identical cartridge 5B path does not. The
entire point of `nsf_expansion` is that an NSF tune sounds bit-for-bit like
the cartridge, so that function is now `i32` and unclamped rather than
silently diverging the two paths.

The snapshot layer was blind to expansion audio
-----------------------------------------------

Found by accident, and worse than the bug it was hiding: the 5B level moved
by 18.5x and ALL SIX 5B snapshots stayed byte-identical.

Snapshots captured 120 frames. These ROMs hold a 2A03 reference tone first
and do not switch the expansion chip in until ~frame 560 -- so the
"load-bearing audio sentinel" hashed boot plus the reference section and had
never once observed the chip under test. The module docs asserted it caught
"any regression in the VRC6/VRC7/N163/5B/MMC5 audio path"; that was false.

Fixed by covering the expansion segment, and the last two needed per-ROM
windows rather than a bigger shared one. Instrumenting the 5B register file
(new `5b_*` rows in the FME-7 debug window) measured each ROM's first
non-zero 5B output:

  db_5b        frame ~540      envelope_5b  frame ~420
  noise_5b     frame ~900      sweep_5b     frame ~4740  (~79 s)

Neither late ROM is broken and neither awaits input -- my first guess. The
upstream README confirms they are not hotswap ROMs, so the delay is not a
hotswap wait: `noise_5b` enables noise on channel A (mixer `$37`, volume 12)
~15 s in, and `sweep_5b` runs a slow volume sweep holding mixer `$3F` (the
wiki's "both bits set => constant output at volume" case) from ~79 s in.
Hence `SNAPSHOT_FRAMES` = `DB_FRAMES` for the corpus, with `NOISE_5B_FRAMES`
/ `SWEEP_5B_FRAMES` for the two stragglers.

All 19 snapshots re-blessed: the hash changes are the window extension plus,
for the 5B ROMs, the level fix. Verified by perturbation, not asserted -- a
ONE-UNIT scale change (0.04%) now fails all six 5B snapshots, where an 18.5x
change previously failed none.

mapper-audio OFF was broken, and is now gated
---------------------------------------------

Surfaced while checking this change against the feature matrix.
`Namco163Audio` was missing the feature-off `clock()` shim that the NSF
expansion router calls unconditionally, so `-p rustynes-mappers
--no-default-features` failed outright with `E0599`. Nothing noticed because
nothing built it: every other gate turns features ON, and the `no_std` job
is `-p rustynes-core`, which keeps `mapper-audio` on.

Why a shim at all: the feature's contract (crate manifest) is that with
audio off the register decoders stay live -- so save states from an
audio-enabled build still load -- while the oscillators freeze. The router
calls `clock()` on every present chip with no `cfg` of its own, so the
method must EXIST in both configurations and do nothing in one. FDS and
Sunsoft 5B already had the pair; the N163 had only `mix`.

`cargo clippy -p rustynes-mappers --no-default-features --all-targets
-D warnings` now runs in the `lint` job so the subtraction case cannot rot
again. Making it pass needed two narrow allow sets, neither of which is
unfinished work -- the DEFAULT build is dead-code-warning clean and every
flagged item has real call sites (`effective_period_p/_s` drive VRC6 period
computation; `half_period` reloads the 5B tone/noise counters). They are
audio-support items unreachable once the subsystem is compiled out, so they
carry `cfg_attr(not(feature = "mapper-audio"), allow(dead_code))` rather
than `cfg` -- they still compile, so a future non-audio caller keeps
working. The shims themselves needed `clippy::unused_self` /
`needless_pass_by_ref_mut` allows: a shim must keep the gated signature so
the unconditional caller compiles, which is exactly what those lints object
to. Most of those sites predate this change and had simply never been
linted.

Also adds the 5B audio register file (mixer `$07`, volumes `$08-$0A`,
envelope period/shape/output, live mix) to the FME-7 mapper debug window.
It was the only part of that board with no debug view, and its mixer/volume
bytes are exactly what answer "why is this cart silent?" -- which is how the
window gap above was diagnosed.

Verification
------------

  cargo test --workspace --features test-roms      2221 passed, 0 failed, 20 ignored
      (= P2's 2220 plus exactly the one new level_db_5b oracle)
  cargo test -p rustynes-mappers --lib             696 passed
  audio_expansion (incl. new level_db_5b)          25 passed
  negative control: 1-unit scale change            all 6 5B snapshots fail
  cargo fmt --all --check                          clean
  cargo clippy --workspace --all-targets -D warnings           clean
  cargo clippy -p rustynes-mappers --no-default-features --all-targets -D warnings   clean (first time)
  cargo clippy -p rustynes-mappers --all-targets -D warnings   clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps    clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features   clean
  actionlint                                       clean
  pre-commit run --files <changed>                 all hooks passed

Docs: the accuracy-ledger row flips to REMEDIATED; `docs/apu-2a03.md` gains
the calibrated level in its table and now lists ONE remaining level gap
(VRC7) instead of two; the `sprint3.rs` prose that justified the deferral is
replaced by the shape/level split it became.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eleven `sprintN.rs` files -- 27,631 lines, ~110 boards -- were named after a
point in the development calendar rather than their contents. `sprint3.rs` held
VRC2, VRC4, VRC6, VRC7, Sunsoft FME-7 and Namco 163; nothing in the name said
so, and nothing said where to add the next Konami board. They are replaced by
board-named modules, and every single-mapper file now carries its iNES mapper
number as an `mNNN_` prefix so the directory sorts by mapper number:
`m000_nrom.rs`, `m004_mmc3.rs`, `m009_mmc2.rs`, `m069_sunsoft_fme7.rs`.

The convention is not invented here. 42 of the 54 existing files were already
board-named (`vrc3.rs`, `sunsoft4.rs`, `taito_x1_005.rs`, `bandai_fcg.rs`);
the sprint files were the outliers. Each new module follows the same shape,
including a hand-written `//!` preamble at the density of its neighbours.

What stayed grouped, and why
----------------------------

Ten files hold ONE implementation covering many mapper IDs -- a shared core
plus a board discriminant. `mmc3_clones.rs` is 11 IDs behind one
`Mmc3CloneMapper`; `multicart_discrete.rs` 27; `bmc_simple.rs` 7; `kaiser.rs`
6. Splitting those would mean copying one implementation N times, so they keep
a plain descriptive name -- no single number describes them, which is also why
they take no prefix.

Boards that were merely ADJACENT are separate files even where a docs argument
could be made for pairing them. MMC2 and MMC4 share the tile-fetch CHR-latch
*concept* and not a line of code -- each has its own `prg_offset`,
`chr_offset`, `update_latch` -- so they are `m009_mmc2.rs` and `m010_mmc4.rs`,
consistent with the `m001_mmc1.rs` / `m004_mmc3.rs` / `m005_mmc5.rs` that
already existed. Same for VRC2 and VRC4, whose only shared code is the small
`vrc_a_bits` pin-rewiring helper, now duplicated per file exactly as the crate
already duplicates `nametable_offset` across ~40 modules.

This moves code; it does not change it
--------------------------------------

Verified mechanically rather than asserted. A checker parses every top-level
item out of the eleven sprint files and the pre-existing mapper files at their
committed revision, locates each in the new tree, and compares the CODE
byte for byte (comment lines excluded, since module docs were deliberately
rewritten):

  499 items from the sprint files  -- byte-identical, 0 missing, 0 altered
  431 items from the renamed files -- byte-identical, 0 missing, 0 altered

The only two reported differences are the intended `crate::mmc3::` ->
`crate::m004_mmc3::` path update in `m118_txsrom.rs` / `m119_tqrom.rs`.

The `parse()` dispatch table resolves the same **172** distinct mapper IDs to
the same constructors -- compared as a set against the committed `lib.rs`, with
none lost, none gained, and none retargeted.

Test count moves 696 -> 701, and every one of the five is accounted for: five
tests each exercised two-to-four different boards in one function
(`save_state_round_trips` covered mappers 15/72/132/96;
`save_load_round_trips_all_four` covered 193/204/221/299). Splitting them
per board is why the count rises, and it means a failure now names the board
instead of a four-board function.

Two mistakes worth recording
----------------------------

The first pass at the numeric rename substituted module names as bare
identifiers, which corrupted 163 sites that merely SHARED a name with a
module: Cargo feature names (`mmc3-m2-phase-irq` -> `m004_mmc3-...`, silently
disabling the gate since a `cfg` on an unknown feature just evaluates false),
struct fields (`vrc7:` -> `m085_vrc7:`), local bindings, ROM paths
(`mmc5/mapper_mmc5test_v1.nes`), ADR filenames, and markdown anchors inside the
frozen `docs/archive/` tree. All 163 were reverted; a detector now proves zero
occurrences of a new module name remain outside a genuine module path or a
`.rs` filename, and `docs/archive/`, `docs/adr/` and `.github/release-notes/`
are byte-identical to their committed state.

The second: one hand-written test helper (`synth_prg_16k`) was retyped instead
of copied, filling with `0x00` where the original filled `0xFF` -- which turned
the bus-conflict AND into a no-op and failed four Jaleco tests. Copied verbatim
thereafter.

Also renamed, same reason
-------------------------

  tests/roms/sprint-2/ -> tests/roms/assorted/   (a mixed blargg/kevtris corpus
      spanning CPU timing, OAM, reset, APU length counters, palettes and
      nestest -- grouped by "everything else", not by a sprint)
  m78.rs -> m078_irem_jaleco78.rs                (every peer uses a vendor name)

A project-wide sweep for the same process-vs-purpose problem found nothing else
in the live tree: `instr_misc.rs` mirrors the upstream blargg suite name,
`batch_runner.rs` runs a batch, `to-dos/libretro/SPRINT_PLAN.md` is a sprint
plan, and no file anywhere carries a version or date in its basename.
`scripts/diag/` (`an2.py`, `cmp2007c.py`, `diff2004_new.py`) is left alone by
design -- its README declares it salvaged ad-hoc scratch and indexes it by
purpose, the same status as the other frozen trees.

Docs updated in the same change: `docs/mappers.md`, `docs/STATUS.md`,
`docs/accuracy-ledger.md`, `to-dos/ROADMAP.md`,
`to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`, `tier.rs`, `unif.rs`, and the
`tests/roms/` READMEs + `LICENSES.md`.

Verification
------------

  cargo test --workspace --features test-roms      2226 passed, 0 failed, 20 ignored
      (= A1's 2221 plus exactly the +5 from splitting five multi-board tests)
      AccuracyCoin 141/141, nestest 0-diff, holy_mapperel and the commercial
      oracle unmoved
  cargo test -p rustynes-mappers --lib                         701 passed
  cargo fmt --all --check                                      clean
  cargo clippy --workspace --all-targets -D warnings           clean
  cargo clippy -p rustynes-mappers --no-default-features ...   clean
  cargo clippy -p rustynes-frontend --features retroachievements  clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps    clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features   clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ame scripts/diag

Two loose ends from the mapper re-split.

Numeric prefixes for single-board, multi-ID modules
---------------------------------------------------

Five modules implement exactly ONE board that answers to several iNES mapper
numbers, so the previous "more than one ID means no prefix" rule left them
unnumbered even though a number describes them fine. They take their lowest ID:

  vrc4       -> m021_vrc4        (21, 23, 25 -- one chip, three PCB pin-rewirings)
  vrc6       -> m024_vrc6        (24, 26)
  namco118   -> m088_namco118    (88, 206)
  bandai_fcg -> m016_bandai_fcg  (16, 159)
  jy_asic    -> m035_jy_asic     (35, 90, 209, 211)

The ten modules that hold MANY DISTINCT boards behind one shared core keep
plain names, unchanged: `mmc3_clones.rs`, `multicart_discrete.rs`,
`bmc_simple.rs`, `kaiser.rs`, `sachen_8259.rs`, `ntdec.rs`, `waixing.rs`,
`sachen_discrete.rs`, `homebrew_boards.rs`, `jaleco_discrete.rs`. No single
number describes those.

The rename was done with a PRECISE matcher this time -- `mod X;`, `use X::`,
`pub use X::`, `crate::X`, and `X.rs` only -- after the previous pass corrupted
163 sites by substituting bare identifiers. Confirmed by inspection that the
`let vrc6 = Vrc6::new(...)` binding in `lib.rs` and the `vrc6:` field in
`nsf_expansion.rs` are untouched, which is exactly what the earlier blanket
regex got wrong.

Also fixed seven doc references to `vrc2_vrc4.rs`, a file that no longer exists
(it split into `m022_vrc2.rs` + `m021_vrc4.rs`). The four that cite the
"CPU-cycle IRQ family pattern" now point at `m021_vrc4.rs` specifically rather
than the old pair, because VRC2 has no IRQ counter -- the old reference was
imprecise even before the split.

scripts/diag/ renamed by purpose
--------------------------------

The archive stays an archive -- no script's behaviour changed -- but an archive
nobody can navigate is not serving its purpose. Nineteen files renamed after
reading what each actually computes:

  an.py       -> ppu2002_read_value_histogram.py
  an2.py      -> ppu2002_isolated_exact_timed_reads.py
  an3.py      -> ppu2002_prerender_vbl_reads.py
  ctx2002.py  -> ppu2002_read_context_window.py
  diff2002.py -> ppu2002_diff_traces.py
  cmp3.py     -> ppu2004_mesen_vs_rustynes_scanline128.py
  diff2004.py -> ppu2004_diff_vs_answerkey.py
  diff2004_new.py -> ppu2004_diff_vs_answerkey_rerun_capture.py
  cmp2007.py  -> ppu2007_key_alignment_shift_scan.py
  cmp2007b.py -> ppu2007_fetch_bus_dump.py
  cmp2007c.py -> ppu2007_phase_offset_ranking.py
  cmp2007d.py -> ppu2007_pattern_value_sets.py
  eval2007.py -> ppu2007_stress_per_index_evaluator.py
  greedy.py   -> ppu2007_offset_greedy_search.py
  parse.py    -> dump_trace_csv.py
  findtest.py -> locate_first_trace_divergence.py
  RustyNES_v2_analyze_span.py -> dma_loop_span_analyzer.py
  RustyNES_v2_sweep.py        -> dma_sweep_analyzer.py

Two findings from reading them rather than guessing:

- `cmp3.py` opens `diff2004.py` BY FILENAME to parse the canonical 341-byte
  `AnswerKey1` back out of its source. A blind `git mv` would have broken it
  silently -- it fails only at run time, and nothing runs these in CI. The
  reference is updated and the extraction re-verified (341 bytes recovered).
- `diff2004.py` and `diff2004_new.py` are byte-identical except for one string:
  the input CSV (`reg_2004stress.csv` vs `..._new.csv`). That is a parameter,
  not a variant. Preserved as two files rather than parameterised, because these
  are captured artifacts and rewriting them would falsify the record -- but the
  README now says so outright instead of leaving a reader to diff them.

The README was also wrong in three ways, now fixed: it filed `greedy.py` under
"Cross-diff / DMA-tail" when it is a `$2007` offset search; it listed
`RustyNES_analyze_span.py` / `RustyNES_sweep.py` without their real `_v2`
prefix; and it listed a `floor.sh` that does not exist. It now carries a
one-line description per script, and every script on disk is listed.

The `RustyNES_v2_*` prefix referred to upstream ENGINE LINEAGE, not this repo's
version -- exactly the confusion the project's versioning note warns about --
so those two are named for the CSV dumps they analyse instead.

Verification
------------

  cargo test -p rustynes-mappers --lib                        701 passed
  cargo fmt --all --check                                     clean
  cargo clippy --workspace --all-targets -D warnings          clean
  RUSTDOCFLAGS="-D warnings" cargo doc -p rustynes-mappers    clean
  every scripts/diag/*.py parses (ast.parse)                  clean
  README <-> disk cross-check                                 every script listed, every listing exists
  no reference to an old diag name outside scripts/diag       confirmed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(A2)

Closes the last Holy Mapperel WRAM residual attributable to FME-7. Also records
the P3 and P4 performance investigations, both of which measured out below the
adoption bar and adopted nothing.

FME-7 (mapper 69) — the fourth register state
---------------------------------------------

Command `$8` carries RAM-enable in bit 7 and RAM-select in bit 6. Three of the
four states already worked: both set maps PRG-RAM, and bit 6 clear maps a
PRG-ROM bank regardless of bit 7. The fourth — **selected but not enabled**
(bit 6 = 1, bit 7 = 0) — drives *neither* chip on real hardware: the RAM is
addressed with its enable deasserted, and the ROM is deselected. The databus is
left floating, so the read returns the open-bus latch.

RustyNES instead fell through to the PRG-ROM bank and returned that bank's tag
byte `1`. Holy Mapperel's WRAM sub-check requires the floating read to be `>= 3`
(a `$7F`-class byte), so it set `MAPTEST_WRAMEN` — the entire `1000` nibble both
`M69_*` ROMs reported.

Routed through `Mapper::cpu_read_unmapped` rather than by returning a guessed
byte from `cpu_read`. That is the trait's existing contract for "not wired to
mapper-resident memory", and it is the semantically correct one: the bus
*preserves* the open-bus latch instead of clobbering it, which is what open bus
actually is. No new trait surface; the default impl already handled the
`$4020-$5FFF` case this override now extends.

Verified rather than asserted:

- Direct probe: the RAM-selected-but-disabled window reads `$7F` (was the bank
  tag `1`); the enabled window still reads RAM and the deselected window still
  reads the ROM bank.
- **Negative control** — reverting the change flips the on-screen result digit
  back from `0` to `1`. The Holy Mapperel `detail` string in the harness is a
  hand-maintained classification, not a measured value, so the screen itself was
  decoded (rendered to ASCII) rather than trusting the label.
- `M69_*` detail `1000` -> `0000`; both snapshot hashes re-blessed, and the
  harness classification + module docs + `docs/accuracy-ledger.md` updated from
  "deferred residual" to remediated.
- **Commercial oracle: no regression.** Run with and without the change, the
  `--features commercial-roms` suite reports the identical set of failures, and
  the FME-7 titles (Mr. Gimmick, Batman: Return of the Joker) are unaffected.

MMC1 WRAM write-protect is NOT in this change. It is the other half of A2 and
the riskier one — a notorious game-compatibility hazard that Holy Mapperel's own
README notes FCEUX and PowerPak also omit — and validating it needs the very
commercial-audio oracle the next section shows is currently stale. Adjudicating
a compatibility question against a broken baseline is worse than not doing it.

Pre-existing finding: the commercial oracle's audio rows are STALE
------------------------------------------------------------------

Surfaced while validating the above, and reported rather than quietly blessed.
Seven `external_real_games` cases fail on `audio_fnv1a64` alone — framebuffer
hashes, cycle counts and sample counts are all identical:

  external_fme7_mr_gimmick, external_mmc5_{castlevania_3, bandit_kings,
  uchuu_keibitai_sdf}, external_vrc6a_castlevania_3_retranslation,
  external_vrc6b_{esper_dream_2, madara}

They are not regressions from this session. The VRC6 and MMC5 snapshots were
last blessed **2026-06-13**; `VRC6_MIX_SCALE` was recalibrated 256 -> 979 in
**v2.1.6** (`fd82485c`, 2026-07-11), which necessarily moved VRC6 audio. Those
vectors have therefore been stale for weeks. The A1 5B calibration in `819b1bd0`
moves the Gimmick row for the same reason. Confirmed by reading the diffs: A1's
`bus.rs`/`mmc5.rs` edits are value-preserving (`f32::from(i16)` ->
`i32 as f32`; MMC5 still computes in `i16` then widens), so they cannot explain
the VRC6/MMC5 rows.

Root cause is structural: this suite needs `--features commercial-roms` AND
local gitignored ROM dumps, so neither CI nor the default local gate
(`--features test-roms`) ever runs it. A golden vector nothing executes is not a
gate. Left unblessed deliberately — module 20's rule is that a canonical vector
changes only on an intentional, reviewed change, and re-blessing seven rows for
behaviour that landed in someone else's release is a maintainer's call, not a
drive-by. It is now written down instead of rediscovered.

P3 — `emit_pixel` (decision: REJECTED, reverted)
------------------------------------------------

`perf annotate` showed the cost is not the pixel math but the *stores*:
`framebuffer: Box<[u8]>` has a runtime length, so the optimiser emits a bounds
check and panic path per pixel, twice, 61,440 times a frame. Candidate: fixed-
size boxed arrays plus a branchless clamp so both checks fold away.

It makes the shipped default **slower**. Same-runner Criterion A/B (a worktree
at HEAD benched against the working tree through one shared target dir):
`nestest_fast` **+4.32%** (p=0.00) and `flowing_palette_fast` **+3.35%**
(p=0.02) — and since P1 promoted the fast dot path to the default, those are the
configuration that ships. The one favourable number (−3.10% on the now-non-
default exact path) is not significant (p=0.09, CI spans zero). Reverted.

Two lessons in `docs/performance.md`: perf's self-time *percentage* is not a
verdict (`emit_pixel` measured *higher* after the change, 10.26% vs 9.38% — a
share, not a duration), and removing a well-predicted bounds check is not free.

P4 — `cpu_clock` (decision: no change adopted)
-----------------------------------------------

The plan expected DMA/PPU hook overhead. `perf annotate` showed the hot
instructions are floating point — the APU inlined through `apu_advance_one`.
Both textbook optimizations are **already implemented**: the per-channel gain
short-circuits at unity, and the FIR scatter is already guarded by
`delta != 0.0`.

A first probe stubbing `mixed` to a constant showed a 6.9-7.9% "win" — an
artifact: a constant lets LLVM prove `delta == 0.0` and delete the whole
band-limited scatter, so it measured synthesis, not the mixer. The clean
measurement adds a SECOND, discarded `black_box`ed mixer call, leaving the value
into `add_sample` untouched: **+1.9%**. That is the hard ceiling for caching the
mix across unchanged cycles, before paying for change detection — below the 3%
bar, and it would need new per-instance state that the v2.2.3 schema audit makes
a save-state decision. Nothing adopted, nothing reverted (probes were throwaway).

Verification
------------

  cargo test -p rustynes-mappers --lib                        701 passed
  cargo test -p rustynes-test-harness --test holy_mapperel    1 passed (re-blessed)
  external_real_games (commercial-roms)                       53 passed / 7 failed,
      an IDENTICAL set with and without this change (all pre-existing, above)
  cargo fmt --all --check                                     clean
  cargo clippy --workspace --all-targets -D warnings          clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features  clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cle audio

Completes A2 and closes the LAST two Holy Mapperel residuals — all 17 ROMs now
report `detail=0000`. Also re-blesses seven commercial-oracle rows that had gone
silently stale, and adds a CI-visible tripwire so that cannot recur.

MMC1 software WRAM write-protect (both layers)
----------------------------------------------

MMC1 carries **two** software PRG-RAM write-protect layers; RustyNES modelled
neither, reading and writing `$6000-$7FFF` unconditionally.

* **`$E000` bit 4** — the disable common to every MMC1 board.
* **SNROM's second layer** — on a CHR-**RAM** board the CHR bank register's
  bit 4 is wired to the RAM's other enable. Gated on `chr_is_ram`, because on a
  CHR-ROM board (SJROM etc.) those same bits are real CHR banking. Getting that
  gate wrong would break every SUROM/SJROM title, so it has its own negative-
  control test.

That distinction is exactly what Holy Mapperel measured: `1000` on SJROM (the
`$E000` layer alone) versus `5000` on SNROM (both,
`MAPTEST_WRAMEN2 $40 | MAPTEST_WRAMEN $10`).

A disabled window now reports `cpu_read_unmapped`, so the read floats to open
bus rather than returning stale RAM — the same trait contract the FME-7 fix used
in the previous commit — and writes are discarded.

**This is the change the plan flagged as a game-compatibility hazard**, which
Holy Mapperel's own README notes FCEUX and PowerPak decline to model. So it was
validated before landing, not assumed:

  commercial oracle          60/60   incl. SEVEN battery-backed MMC1 saves
                                     (Zelda, Metroid, Final Fantasy, Mega Man 2,
                                     Castlevania II, Ninja Gaiden, Kid Icarus —
                                     precisely the titles that corrupt if the
                                     RAM enable is mismodelled)
  extended commercial corpus 138/138
  Holy Mapperel              17/17 detail=0000 (was 15/17)

No regression, so it ships on by default rather than behind the default-off knob
the plan held in reserve. Verified by negative control: reverting flips the
on-screen result digit back from `0` to `1`. The `detail=` string in the harness
is a hand-maintained classification rather than a measurement, so the screen
itself was decoded to ASCII instead of trusting the label. Three new unit tests
pin both layers plus the CHR-ROM negative control.

The stale commercial-oracle audio rows
--------------------------------------

Seven `external_real_games` rows failed on `audio_fnv1a64` **alone** — `frames`,
`cycles`, `audio_samples` and every `checkpoint fb_fnv1a64` byte-identical. All
14 changed snapshot lines are `audio_fnv1a64`; nothing else moved. Attribution,
established from history rather than assumed:

* **MMC5 ×3, VRC6 ×3** — `VRC6_MIX_SCALE` and all three MMC5 level constants
  last changed value in `fd82485c` (v2.1.6, 2026-07-11). These snapshots were
  blessed in `a61aee08` on **2026-06-13**, 28 days earlier. Stale since v2.1.6,
  across several releases.
* **FME-7 (Mr. Gimmick)** — moved by this line's own A1 5B calibration
  (`819b1bd0`). Gimmick! is the 5B showcase title.

Re-blessed with that provenance recorded. The emulated timeline is untouched in
every case; only the mixed audio level changed, deliberately, upstream.

Root cause was structural, so the structural hole is now plugged
----------------------------------------------------------------

This suite needs `--features commercial-roms` **and** local gitignored ROM dumps,
so neither CI nor the default `--features test-roms` gate can ever run it. A
golden vector nothing executes is not a gate — it just accumulates drift until
someone runs it by hand.

`expansion_level_tripwire` (in `rustynes-mappers`, a plain unit test CI **does**
run) pins every expansion-audio level constant: VRC6, MMC5 pulse/PCM, N163, and
both Sunsoft 5B scale terms, plus the derivation of `MMC5_MIX_BIAS` from its two
scales so the formula cannot drift while the scales stay put. It asserts nothing
about correctness — the decibel oracles do that. Its only job is to make a
silent change loud, and its failure message names both suites that must be
re-blessed in the same change.

Negative-controlled: perturbing `VRC6_MIX_SCALE` by one fails it with that
message. `NAMCO163_MIX_SCALE` and the two 5B constants were `pub(crate)`-ised so
the tripwire can see them.

Verification
------------

  cargo test -p rustynes-mappers --lib                        706 passed
  holy_mapperel                                               17/17, all detail=0000
  external_real_games      (commercial-roms)                  60/60
  external_extended        (commercial-roms)                  138/138
  negative control: MMC1 revert                               on-screen digit 0 -> 1
  negative control: VRC6_MIX_SCALE +1                         tripwire fails as designed
  cargo fmt --all --check                                     clean
  cargo clippy --workspace --all-targets -D warnings          clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features  clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Zapper's photodiode charges as the CRT beam passes the aim point and drains
over ~19-26 scanlines. RustyNES sampled the *completed* framebuffer once per
frame, so the light bit was constant for the whole frame: a game polling
immediately after its flash and one polling 100 scanlines later got the same
answer. That is not a resolution shortfall, it is a category error — the
frame-granular model structurally cannot express intra-frame timing.

`ZapperState::light_at_scanline` models it directly:

* before the beam reaches the aim row — dark, this frame has not painted it;
* from the aim row until `ZAPPER_LIGHT_HOLD_SCANLINES` elapses — bright iff the
  aperture is bright;
* after the hold — dark, the capacitor having drained.

Two design choices worth stating.

**It holds no extra state.** Light is derived on demand at read time from
`(framebuffer, aim, current scanline)` rather than latched by a per-scanline
callback. So it adds no field to serialize, cannot desync a save state or a
netplay rollback, and keeps the determinism contract — the same inputs always
give the same answer. It also needs no new PPU hook: `read_at_scanline` takes
`&self` and the PPU is a different field of the bus, so they are disjoint
borrows.

**Both models share one aperture test.** `aperture_is_bright` was factored out
of `sample_light`, so the temporal path can differ from the frame path only in
*when* it samples, never in what counts as light. A lone bright pixel still
fails to charge the sensor at the exact beam position.

One consequence is physically right rather than a compromise: the aperture rows
*below* the beam still hold the previous frame's pixels, which is exactly what
the sensor sees, since the beam has not repainted them yet.

Why it stays opt-in
-------------------

There is **no redistributable pass/fail light-gun test ROM** to adjudicate this.
The supported titles re-poll every frame and are satisfied by either model, so
promoting it would change output with no oracle able to confirm the change is an
improvement — and the project's standing bar is that an accuracy change is
oracle-proven or default-off. `Nes::set_zapper_temporal_light` defaults to
`false`, and with it off the shipped `$4016`/`$4017` byte is exactly what it was.

Five unit tests cover the model, including the one that states its whole point:
`zapper_frame_model_is_scanline_invariant_but_temporal_is_not` asserts the frame
model gives one answer while the temporal model gives three different answers
within that same frame. The existing four Zapper tests pass unchanged, which is
what shows the `aperture_is_bright` extraction was behaviour-preserving.

Verification
------------

  cargo test --workspace --features test-roms   2237 passed, 0 failed, 20 ignored
      AccuracyCoin 141/141, nestest 0-diff, visual_regression 9/9,
      input_devices 8/8 — the default path is untouched
  cargo fmt --all --check                                      clean
  cargo clippy --workspace --all-targets -D warnings           clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps    clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features  clean

Docs: `docs/accuracy-ledger.md` Zapper row updated, `docs/frontend.md` replaces
its "documented future refinement" paragraph with the landed model, CHANGELOG
entry added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Version ceremony for the performance-appraisal and accuracy-closure line:
workspace `2.2.2 -> 2.2.3`, CHANGELOG `[Unreleased]` cut to `[2.2.3]`, and the
maintainer-authored release notes that `release-auto.yml` will publish as the
GitHub Release body.

A *datum* is the fixed reference a measurement is taken from, which is what this
release is about: promoting a win that was already proven, pinning the
references that keep it honest, and closing the two accuracy residuals that had
a real oracle waiting for them.

What ships
----------

**Performance.** The specialized PPU fast dot path becomes the default and is
reachable from the frontend for the first time -- `Nes::set_fast_dotloop` had
zero callers outside the core, so a measured **-11.3%** frame-time win shipped
switched off. Release builds now carry **PGO-optimized** Linux binaries when the
existing >3%-and-byte-identical gate passes, and CI gained a same-runner
relative frame-time gate closing a hole where a 2.5x slowdown passed the
deliberately-loose absolute ceiling. Two further optimizations were **measured
and rejected**, published with their numbers.

**Accuracy.** The last two Holy Mapperel residuals are closed (17/17 now
`detail=0000`): MMC1's two WRAM write-protect layers and FME-7's open bus on the
RAM-selected-but-disabled window. The Sunsoft 5B absolute level is calibrated
against Mesen2, which required widening `Mapper::mix_audio` to `i32`. A
save-state schema gap is fixed (`PPU_SNAPSHOT_VERSION` 8 + an APU v4 tail) --
the reason AccuracyCoin now reports 141/141 through run-ahead as well as
without it. A Zapper beam-relative light model lands opt-in.

**Housekeeping.** The eleven `sprintN.rs` mapper modules are renamed for the
boards they emulate with `mNNN_` prefixes, and seven commercial-oracle audio
rows stale since v2.1.6 are re-blessed behind a new CI-visible tripwire.

Version surfaces updated
------------------------

`Cargo.toml` workspace version (all 18 crates inherit it -- verified via
`cargo metadata`, every package now reports 2.2.3), the README badge and its
Current Release section, `docs/STATUS.md`'s authoritative current-state block,
and the three `AGENTS.md` paragraphs that state the current release including
the "never claim later than" guard.

**Mobile versions are deliberately NOT bumped.** Android `versionCode` /
`versionName` and iOS `MARKETING_VERSION` stay frozen at their v2.0.x host-only
release points; the joint store launch remains v2.3.0. Same treatment as v2.2.2.

Release mechanics
-----------------

The tag and GitHub Release are created by `release-auto.yml`, NOT by hand: on
merge to `main` it waits for CI to go green, then tags `v2.2.3`, publishes with
`.github/release-notes/v2.2.3.md` as the body, and invokes `release.yml` via
`workflow_call` to build and attach the Linux / macOS-aarch64 / Windows
binaries. Tagging manually would make that job a no-op (it skips when the tag
already exists) and skip the binary build entirely.

Dry-ran the workflow's own resolution logic against this tree rather than
assuming it: the `awk` version extractor reads `2.2.3` from `[workspace.package]`,
and the CHANGELOG header parses to the title

  RustyNES v2.2.3 — "Datum" (fast dot path promoted + PGO shipped + the last
  two mapper residuals closed)

Repo hygiene caught by the ceremony
-----------------------------------

An earlier `git add -A scripts/` in this line swept in files that should not
have been committed, found by auditing the release surface rather than by a
test:

- `scripts/probes/check_dirs.rs` -- a **RustySNES** file. Wrong project
  entirely; it probes `ProjectDirs::from("io.github", "doublegate", "RustySNES")`.
  Removed.
- `scripts/probes/dsp_debug_test.rs` -- a one-line file whose entire content is
  a comment saying it is temporary debug scratch. Removed.
- `scripts/mapper-promotion/{ids,show252}.py` -- unrunnable, hardcoding an
  absolute path into a `/tmp` session scratchpad that no longer exists. Removed.
- `scripts/mapper-promotion/{threads,showthreads}.py` -- not mapper work at all;
  they filter the GitHub GraphQL `reviewThreads` payload. Moved to
  `scripts/pr-review/` as `list_all_threads.py` / `list_unresolved_threads.py`,
  which is where they are actually used (the bot-comment closeout ceremony).

The eight genuine v2.1.0 F3 tier-promotion scripts are kept and given the README
the project's own convention requires for salvaged ad-hoc tooling, matching
`scripts/diag/` and `scripts/gg/` -- including the standing rule that the ROM
dumps they operate on are never committed.

`.gitignore` needed no change: it already covers the artifact classes this line
produces (criterion output, `perf.data`, `*.profraw`/`*.profdata`, `/pgo-data/`,
`/target/pgo/`, `*.snap.new`), and `bench_relative_check.sh` builds its
comparison worktree under `mktemp -d`, outside the repo. The problem was files
that should never have been added, not missing ignore rules.

Verification
------------

  cargo test --workspace --features test-roms      2237 passed, 0 failed, 20 ignored
      (AccuracyCoin 141/141, incl. through run-ahead; nestest 0-diff;
       holy_mapperel 17/17; pal_apu_tests 10/10; visual_regression 9/9)
  cargo fmt --all --check                          clean
  cargo clippy --workspace --all-targets -D warnings              clean
  cargo clippy -p rustynes-mappers --no-default-features ...      clean
  cargo clippy -p rustynes-frontend --features retroachievements  clean
  RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps      clean
  cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features  clean
  actionlint                                       clean
  markdownlint (all changed/new markdown)          clean
  cargo metadata                                   every crate at 2.2.3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aee87a84-65be-4ecf-be9c-baa9f6ffcc16

📥 Commits

Reviewing files that changed from the base of the PR and between d2bdccd and 839eafc.

📒 Files selected for processing (2)
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
📝 Walkthrough

Walkthrough

This PR is a large v2.2.3 "Datum" release combining a mechanical mapper-crate reorganization (sprint-numbered modules renamed to board-named mNNN_*.rs files) with widened Mapper::mix_audio (i16→i32), default-on PPU fast-dotloop, an opt-in idle-line fast path, APU/PPU save-state version bumps (v4/v8) with a new schema-audit test, an opt-in Zapper temporal light model, CI/PGO/release workflow updates, a test-ROM directory rename (sprint-2→assorted), and extensive documentation/ad-hoc tooling updates.

Changes

Mapper Crate Restructure and Audio Widening

Layer / File(s) Summary
Dispatch and shared audio contract
crates/rustynes-mappers/src/lib.rs, mapper.rs, tier.rs, unif.rs, nsf.rs, nsf_expansion.rs, tests/corpus.rs
parse() now dispatches to new mNNN_* modules; Mapper::mix_audio and NSF/expansion mixers widen from i16 to i32.
Mapper module renames (m000-m513) + shared multicart frameworks
crates/rustynes-mappers/src/m0*.rs...m5*.rs, homebrew_boards.rs, jaleco_discrete.rs, kaiser.rs, bmc_simple.rs, mmc3_clones.rs, multicart_discrete.rs, ntdec.rs, sachen_*.rs, waixing.rs
Sprint-numbered files renamed/rewritten into board-named modules with mNNN_ prefixes; MMC1 WRAM write-protect and FME-7 open-bus fixes included.
Accuracy tests/docs
tests/holy_mapperel.rs, tests/audio_expansion.rs, docs/mappers.md, docs/compatibility.md
Holy Mapperel expectations updated to PASS for MMC1/FME-7; expansion-audio level oracle added for Sunsoft 5B; docs updated to new module paths.

Estimated code review effort: 5 (Critical) | ~150 minutes

PPU/Core Performance Defaults

Layer / File(s) Summary
Feature flags and PPU tick dispatch
rustynes-ppu/Cargo.toml, src/ppu.rs, rustynes-core/Cargo.toml, nes.rs
Adds ppu-idle-line-fast feature; fast_dotloop defaults to true; new tick_idle_line_fast dispatch path.
Frontend wiring and regression test
rustynes-frontend/src/{app,config}.rs, debugger/settings_panel.rs, fast_dotloop_diff.rs, docs/performance.md, docs/ppu-2c02.md
Live-apply UI checkbox; synthetic vblank torture-ROM differential test; documentation of the promotion decision.

Save-State Schema (APU v4 / PPU v8)

Layer / File(s) Summary
APU/PPU snapshot bumps
rustynes-apu/src/snapshot.rs, rustynes-ppu/src/snapshot.rs, docs/adr/0034-*.md
APU_SNAPSHOT_VERSION 3→4 ($4017 rewrite tail); PPU_SNAPSHOT_VERSION 7→8 (sprite-eval FSM).
Schema audit and run-ahead test
tests/snapshot_schema_audit.rs, tests/accuracycoin_runahead.rs, docs/accuracy-ledger.md, docs/STATUS.md
Compile-time audit ensures every chip field is serialized or excluded; run-ahead battery regression.

Zapper Temporal Model

Layer / File(s) Summary
Beam-relative light detection
rustynes-core/src/{bus,input_device,nes}.rs, docs/frontend.md
Opt-in zapper_temporal_light computes light bit from beam scanline instead of per-frame sampling.

Test ROM Rename and CI/Release

Layer / File(s) Summary
sprint-2 → assorted rename
benches, netplay/frontend/harness tests, tests/roms/assorted/*
All ROM path references and vendored binaries moved under assorted/.
CI/PGO/release workflows
.github/workflows/*.yml, .github/actionlint.yaml, scripts/bench_relative_check.sh, scripts/agy-review.sh
Relative bench gate, reusable PGO workflow, Linux PGO asset promotion on release.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • doublegate/RustyNES#324: Both PRs touch the Antigravity self-hosted reviewer workflow — this PR updates inline docs/comments in .github/workflows/antigravity-review.yml, while #324 introduces that workflow and its scripts/agy-review.sh machinery.
🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Changelog Entry For User-Visible Changes ⚠️ Warning PR has user-visible changes, but CHANGELOG.md leaves [Unreleased] empty and places the new notes under [2.2.3] instead. Add an entry under [Unreleased] summarizing the user-visible changes, or move the new notes there before release.
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main release actions: v2.2.3 Datum, fast dot path promotion, PGO shipping, and closing the final mapper residuals.
Docstring Coverage ✅ Passed Docstring coverage is 83.02% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Docs-As-Spec Sync ✅ Passed PPU, APU, and mapper behavior changes each have matching docs updates in docs/ppu-2c02.md, docs/apu-2a03.md, and docs/mappers.md; no scoped behavior change lacked doc coverage.
No Unwrap/Expect/Panic On Untrusted Input ✅ Passed No new unwrap/expect/panic on untrusted inputs in non-test code; bus.rs uses try_from+match, scripts return SystemExit, and remaining hits are tests only.
Safety Comment On New Unsafe Blocks ✅ Passed No new unsafe {} blocks or unsafe fns were found in the 136 changed Rust files, so no adjacent // SAFETY: comments were required.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/fast-dotloop-promote-and-expose

Comment @coderabbitai help to get the list of available commands.

doublegate and others added 2 commits July 23, 2026 10:32
…I limit

The `review` check failed on PR #325 in 6 seconds:

  could not find pull request diff: HTTP 406: Sorry, the diff exceeded the
  maximum number of lines (20000)

Not a defect in the PR. GitHub's API refuses to serve any diff over **20,000
lines**, and that PR's mapper re-split moves 27,631. `agy-review.sh` treated
every `gh pr diff` failure as fatal, so "this PR is too big to fetch" produced
the same red X as an auth or network error.

Those are different conditions and must not be conflated. A reviewer that cannot
see the diff must not claim a verdict -- but it also must not fail the build: a
red check that means "the PR was large" is noise, and noise trains people to
ignore the check. It now distinguishes the two, logging the reason and exiting 0
on the size case while a genuine error still fails loudly (and now prints the
underlying stderr instead of swallowing it, which is why the original failure
took a log dive to diagnose).

This is upstream of the existing `MAX_DIFF_BYTES` truncation, which can only
shrink a diff already in hand -- it could never have helped here.

`diff_err` is added to the pre-declared temp-file list so the `EXIT` trap stays
safe under `set -u`, matching how every other temp file in the script is handled.

Verified: `bash -n` clean, and the guard matched against the real HTTP 406 text
copied from the failing run rather than a guess at its wording. The three
shellcheck findings in this file (SC1007, SC2015, SC2016) are pre-existing and
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Antigravity reviewer could not review this repository's larger pull
requests. Two independent ceilings stacked, and the second one failed
silently, which is the worse of the two.

GitHub's API refuses any diff over 20,000 lines with HTTP 406, so
`gh pr diff` failed outright on PR #325 (67,770 lines) and the check went
red six seconds in. Beneath that sat a second limit: the assembled prompt
reaches agy as one argv string, and Linux caps a single argument at
MAX_ARG_STRLEN = 32 * PAGE_SIZE = 128 KiB regardless of ARG_MAX, which is
where MAX_DIFF_BYTES=90000 came from. A diff over that budget was
truncated with `head -c`. For a 2.68 MB diff that is a review of the
first 3.3% of the bytes, published under a heading that reads as a review
of the pull request. A confident verdict over an arbitrary prefix is
worse than no verdict, because nothing in the output says which it is.

Rename detection does not rescue this and was measured rather than
assumed: `git diff -M` and `-M -C --find-copies-harder` both return
67,770 lines / 2,686,694 bytes, identical to plain `git diff`. The mapper
re-split rewrote each module (new `//!` preamble, a subset of the
original body), so git sees content change, not renames. Excluding
`Cargo.lock` saves about 3 KB. There is no compression available here;
the diff is genuinely that large.

The fix is to stop treating the argv ceiling as the review's ceiling.
agy is an agent with file-reading tools, so argv bounds only what can be
INLINED. Above MAX_DIFF_BYTES the diff is now written into agy's own
workspace and the prompt points at it:

  * `.agy-review-work/` sits inside the checkout deliberately, so it is
    already part of agy's workspace -- no `--add-dir`, no sandbox
    exception. It is gitignored, created per run, and removed by the exit
    trap, which is guarded on a flag rather than on the directory
    existing so it can never delete a pre-existing path.
  * The diff is split into AGY_DIFF_PART_BYTES (150 KB) parts rather than
    handed over as one 2.68 MB file. A single read that size is at the
    mercy of whatever per-read cap the agent applies, and a silent cap is
    precisely the failure being replaced. The prompt lists every part
    with its line and byte count, and asks for a
    `<!-- coverage: read N/M parts -->` header, so incomplete reading is
    visible in the output instead of invisible in it.
  * The prompt instructs that the parts are consecutive slices of one
    file -- a hunk header can straddle a boundary -- and that an honest
    partial review beats a confident review of an unread diff.
  * AGY_PRINT_TIMEOUT_LARGE (25m) applies only on this path, and only
    when the caller did not set AGY_PRINT_TIMEOUT explicitly. An inlined
    diff arrives with the prompt; a handed-off one must be read first,
    one tool call per part, before reasoning starts. Timing that out
    would reproduce the empty-review failure from the other direction.

Above that, the 406 itself is now distinguished from a genuine failure
(auth, network, bad PR number, which stay fatal) and falls back to a
local `git diff`. It fetches `refs/pull/N/head` and the base branch into
private `refs/agy/*`, computes the merge base, and diffs -- fetching
objects but never checking them out, so the working tree stays on the
default branch and a PR still cannot rewrite its own reviewer. The refs
are deleted by the same exit trap.

That fetch needs credentials without violating the repo-wide
`persist-credentials: false` rule (PR #319), and it needs them in two
different environments. `AUTHORIZATION: bearer` via `http.extraheader` is
what Actions' GITHUB_TOKEN accepts and is tried first; it is rejected
outright by a personal token from `gh auth token` ("remote: invalid
credentials"), so a hand-run falls through to a plain fetch using git's
configured credential helper. Neither path writes anything into
.git/config.

Verified end-to-end against PR #325 through a new AGY_DRY_RUN escape
hatch, which exercises diff acquisition and prompt assembly without
spending an agy run or posting a comment: the API 406 is caught, the
local fallback produces 67,770 lines / 2,686,694 bytes, 18 parts are
written whose sizes sum to exactly the diff, and the assembled prompt is
4,322 bytes against a 120,000-byte cap. shellcheck reports nothing beyond
the pre-existing SC1007 on line 93; actionlint is clean.

Note on rollout: the workflow checks out the default branch, never the PR
head, so this fix does not apply to the check on the PR that introduces
it -- that job keeps running main's copy of the script until this merges.
That is the security property working as designed, not a regression.
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity review (Gemini via Ultra)

This PR updates RustyNES to v2.2.3 by promoting the PPU fast dot path to default, wiring PGO binary promotion and relative frame-time gates into CI, closing MMC1/FME-7 accuracy residuals, calibrating expansion audio levels (widening Mapper::mix_audio to i32), bumping save-state schemas to v8, and reorganizing mapper modules into board-named files.

Blocking issues

None found.

Suggestions

  • Public API breaking change on patch bump: crates/rustynes-mappers/src/mapper.rs#L473 — Changing Mapper::mix_audio return type from i16 to i32 breaks downstream out-of-tree implementers. SemVer dictates a minor version bump (v2.3.0) rather than a patch bump (v2.2.3) when modifying public trait method signatures.
  • Single-point-of-failure in workflow runner: .github/actionlint.yaml#L20 — CI now explicitly depends on a single maintainer self-hosted runner (agy). If that host goes offline or suffers OAuth token invalidation, dependent CI workflows will queue indefinitely without failing fast.
  • Relative bench check error propagation: scripts/bench_relative_check.sh — Verify that benchmark executable failures or missing baseline artifacts explicitly set a non-zero exit code rather than defaulting to pass or reporting a 0.0% delta.

Nitpicks

  • crates/rustynes-mappers/ — Moving ~110 boards out of sprintN.rs into mNNN_*.rs creates a large diff footprint; ensure all internal module docs and developer guides point to the new iNES-numbered file structure.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity review

First, on the review itself: this is the first agy pass that actually saw this PR. The
earlier run failed six seconds in — GitHub's API refuses any diff over 20,000 lines with
HTTP 406, and this one is 67,770. f8974f0c fixes that (local git diff fallback), and
also fixes a quieter problem underneath it: diffs over MAX_DIFF_BYTES used to be
truncated with head -c, which on 2.68 MB would have published a review of the first
3.3% under a heading that reads as a review of the PR. The diff is now handed to agy on
disk in 18 parts, and the <!-- coverage: read 18/18 parts --> header above is the
machine-checkable claim that it read all of it.

1. Mapper::mix_audio i16 -> i32 on a patch bump — VALID, deliberate, already documented

The observation is correct: Mapper is a public trait, and an out-of-tree
impl Mapper that overrides mix_audio with -> i16 will no longer compile. That is a
source-breaking change and the review is right to raise it.

Two corrections and one piece of context:

  • The suggested remedy is itself not SemVer. A breaking change to a public API is
    MAJOR, not MINOR — v3.0.0, not v2.3.0. v2.3.0 would be the wrong number for
    the stated reason.
  • The blast radius is bounded: none of the rustynes-* crates are published to
    crates.io (there is no cargo publish step anywhere in .github/, and no registry
    badge). The only way to be an out-of-tree implementer today is a git dependency on
    this repo.
  • It is already called out for exactly the audience affected —
    .github/release-notes/v2.2.3.md line 117, under Upgrade notes: "Mapper::mix_audio
    now returns i32 (was i16). Out-of-tree mapper implementations need the signature
    updated; every in-tree board returns the values it always did."

Worth separating from the save-state axis, which is governed by ADR 0003 rather than by
Rust-API SemVer: PPU_SNAPSHOT_VERSION 8 and the APU v4 tail are strictly-additive
tails, which ADR 0003 explicitly permits within a PATCH.

Flagged to the maintainer as a version-number decision rather than resolved unilaterally
here. Nothing about the change is accidental or undocumented; the open question is only
whether the number on it should be 2.2.3.

2. Self-hosted runner as a single point of failure — VALID, accepted trade-off

Accurate. runs-on: [self-hosted, agy] does mean the review depends on one host, because
it deliberately uses agy's cached OAuth session rather than a metered API key — that is
the whole reason the reviewer is free under the subscription.

One correction on the proposed mitigation: timeout-minutes would not fail this fast,
because it starts counting when the job starts, not while it waits for a runner. A job
queued against an absent self-hosted runner is bounded by GitHub's own 24-hour queue
timeout, which no workflow setting shortens.

What keeps this from being a merge hazard is that the check is not required — this repo
has no branch protection, so a red or absent review never gates a merge. Treating it as
advisory is the intended posture.

3. Relative bench gate error propagation — already handled; verified rather than assumed

Checked against scripts/bench_relative_check.sh rather than reasoned about:

  • set -euo pipefail (line 44), and neither cargo bench invocation (lines 93 and 99)
    is wrapped in || true, so a bench executable failure aborts the script non-zero.
  • A missing baseline artifact does not degrade to a 0.0% delta. mean_ns returns the
    sentinel MISSING when estimates.json is absent (line 110), and lines 120-124 turn
    that into FAIL: ... criterion estimates missing with rc=1.
  • The two genuine exit 0 paths (lines 59, 64) are deliberate skips with a printed
    reason — an unresolvable base ref (shallow clone / root commit) and base == HEAD. In
    both cases there is no comparison to make, and the absolute ceiling in
    bench_regression_check.sh still applies.

No change made; the suggestion asked for a property the script already has.

Nitpick: references to the new mNNN_<board>.rs layout — done, and verified

The rename was checked for content preservation rather than eyeballed: a byte-for-byte
item comparison across the old and new modules (930 items, 0 altered) and an identical
172-ID dispatch table. Doc and reference updates landed in the same change; docs/,
to-dos/, and the crate-level //! preambles point at the board-named files.

@doublegate

Copy link
Copy Markdown
Owner Author

/gemini review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 55

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/rustynes-apu/src/snapshot.rs (1)

637-742: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider an end-of-input sanity check after decoding, mirroring Ppu::restore.

Apu::restore never verifies r.pos == data.len() after the v4 tail is consumed, unlike Ppu::restore (Line 725 in crates/rustynes-ppu/src/snapshot.rs), which rejects any blob with leftover bytes as Truncated. Since this PR is already touching the tail-parsing logic to add a version-gated field, extending the same trailing-garbage guard here would make truncation/corruption detection consistent across both chips' save-state parsers.

♻️ Proposed fix
             self.reset_4017_delay = 0;
             self.reset_4017_value = 0;
         }
 
+        if r.pos != data.len() {
+            return Err(ApuSnapshotError::Truncated(r.pos));
+        }
         Ok(())
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-apu/src/snapshot.rs` around lines 637 - 742, Add an
end-of-input validation at the end of Apu::restore, after the version-gated
reset_4017_delay and reset_4017_value fields are decoded. Reject any remaining
bytes using the same error behavior as Ppu::restore, while preserving successful
restoration when r.pos equals data.len().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/release-notes/v2.2.3.md:
- Around line 34-40: The release test totals need consistent scope and command
descriptions: update the verification references in
.github/release-notes/v2.2.3.md lines 34-40 and CHANGELOG.md lines 34-40 to
either identify 2,219 as a specific subset of the --features test-roms tests or
replace it with the full 2,237 total, applying the same clarification and
figures in both files.
- Around line 19-21: Update the release note’s explanation of the fast_dotloop
configuration migration to identify default_fast_dotloop() as the mechanism that
enables the option for existing configs, replacing the inaccurate
#[serde(default)] attribution. Preserve the statements about the [emulation]
config key and inherited speedup.
- Around line 105-107: The shared elapsed-day claim is incorrect: June 13, 2026
was 40 days before the July 23, 2026 release. Update the “28 days earlier”
statement in .github/release-notes/v2.2.3.md (lines 105-107) and correct the
corresponding interval or blessing date in CHANGELOG.md (lines 81-87) so both
references are consistent.

In `@AGENTS.md`:
- Line 188: Update the current-release history sentence in AGENTS.md so v2.2.2
“Conduit” is described as the prior release, not “this release.” Remove the
stale current-release wording while preserving the existing v2.2.3
current-release description and historical chain.

In `@crates/rustynes-core/src/bus.rs`:
- Around line 1631-1654: Remove the misplaced port/overlay API documentation
immediately before set_zapper_temporal_light, including its port range and panic
claims; retain only documentation describing the boolean temporal-light behavior
and its default.
- Around line 2499-2508: Update the zapper temporal-light branch in the bus read
path to detect the PPU’s negative pre-render scanline before converting it to
u16, and return the drained/dark temporal value instead of sampling framebuffer
row 0. Preserve normal beam-relative sampling for nonnegative scanlines, and add
a regression test covering a Zapper aimed at row 0 during pre-render.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs`:
- Around line 341-346: The documentation for switchable_high in the PRG window
layout is inverted for mapper 92: update the JF-19 rationale to state that the
reset vectors at $FFFA-$FFFF are in the switchable $C000-$FFFF window,
consistent with m92_uses_5bit_prg_field mapping $C000 to bank 17. Leave the
layout and code unchanged.

In `@crates/rustynes-mappers/src/kaiser.rs`:
- Around line 1-22: Correct the stale mapper documentation and banners without
changing implementation behavior: in crates/rustynes-mappers/src/kaiser.rs lines
1-22, align the module docs with KaiserBoard, add mapper 142/KS7032, use
KS7017=303, KS7031=305, KS7016=306, and KS7013B=312, and remove the orphaned
Waixing253 banner at lines 603-610; in
crates/rustynes-mappers/src/homebrew_boards.rs lines 194-200, retarget banners
at lines 194-200 to 218, 515-520 to 111, and 671-680 to 28, replace the orphaned
Mapper 63/NTDEC banner at lines 1184-1202, and add mappers 28 and 30 to the
module docs; in crates/rustynes-mappers/src/jaleco_discrete.rs lines 52-58,
retarget banners at lines 52-58 to 86, 308-322 to 72/92, and 631-641 to 101,
remove the orphaned Mapper 218 banner at lines 756-766, and add mappers 72, 92,
and 101 to the module docs.

In `@crates/rustynes-mappers/src/m010_mmc4.rs`:
- Around line 242-287: Update save_state/load_state in
crates/rustynes-mappers/src/m010_mmc4.rs (lines 242-287),
crates/rustynes-mappers/src/m021_vrc4.rs (lines 401-458), and
crates/rustynes-mappers/src/m022_vrc2.rs (lines 311-347) to bump the state
version, append each mapper’s prg_ram after the existing serialized data, and
include it in the expected-length calculation and load cursor before restoring
it. Ensure all existing registers, VRAM, and IRQ state continue to round-trip
unchanged.

In `@crates/rustynes-mappers/src/m032_irem_g101.rs`:
- Around line 125-148: Prevent fixed-bank index underflow for single-bank PRG
images by replacing both bank_count - 2 expressions in read_prg in
crates/rustynes-mappers/src/m032_irem_g101.rs:125-148 with saturating
subtraction, and make the same replacement for the slot-2 fixed-bank arm in
crates/rustynes-mappers/src/m033_taito_tc0190.rs:106-117; preserve the existing
modulo bank selection behavior without introducing panics.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs`:
- Line 6: Split the glued Rust doc-comment lines so each sentence ends at its
period and the following //! marker is on its own blank documentation line,
preserving separate rustdoc paragraphs. Apply this in
crates/rustynes-mappers/src/m036_txc_policeman.rs:6,
crates/rustynes-mappers/src/m094_un1rom.rs:5,
crates/rustynes-mappers/src/m095_namcot3425.rs:7, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:6.
- Around line 53-65: Remove the stray mapper banner comment blocks at
crates/rustynes-mappers/src/m036_txc_policeman.rs:53-65 and 189-196;
crates/rustynes-mappers/src/m038_bitcorp38.rs:161-167;
crates/rustynes-mappers/src/m039_subor39.rs:40-52 and 192-203;
crates/rustynes-mappers/src/m041_caltron41.rs:37-43 and 207-216;
crates/rustynes-mappers/src/m094_un1rom.rs:177-182;
crates/rustynes-mappers/src/m095_namcot3425.rs:53-71 and 234-249;
crates/rustynes-mappers/src/m096_bandai96.rs:44-56 and 237-244; and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65 and 221-233. Keep the
correct mapper-38 banner in m038_bitcorp38.rs and leave all implementation code
unchanged.
- Around line 198-199: Remove the redundant second #[cfg(test)] attribute at the
test-module declarations in
crates/rustynes-mappers/src/m036_txc_policeman.rs#L198-L199,
crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185,
crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L235-L236, leaving one attribute
per module.

In `@crates/rustynes-mappers/src/m069_sunsoft_fme7.rs`:
- Around line 922-946: Persist CHR-RAM in both mapper save-state
implementations: in crates/rustynes-mappers/src/m069_sunsoft_fme7.rs lines
922-946, update save_state/load_state to serialize and restore chr when
chr_is_ram, version the existing v1/v2 format safely (for example via v3 or a
conditional tail), and include the CHR length in load_state’s core_expected
while preserving older-state compatibility; in
crates/rustynes-mappers/src/m075_vrc1.rs lines 179-219, append chr after vram in
save_state when chr_is_ram, read it during load_state, and add chr_ram_len to
expected.
- Around line 515-534: Correct the save-state documentation for the tail length:
change the “51 bytes total” summary near the tail layout and the “TAIL_LEN (51
bytes)” note near the TAIL_LEN reference to state 47 bytes. Do not modify
TAIL_LEN, write_tail, or read_tail, since their 47-byte format is authoritative.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs`:
- Around line 55-73: Remove the foreign Action 53 and NTDEC 5-in-1 documentation
blocks from the Namcot 3446 mapper module, preserving only comments relevant to
mapper 76. Restore the missing newline before the module’s `//!` documentation
at the top, and remove the duplicated adjacent `#[cfg(test)]` attribute while
retaining a single test configuration attribute.

In `@crates/rustynes-mappers/src/m079_ave_nina03_06.rs`:
- Around line 53-59: Clean up the copied template residue in the AVE NINA-03/06
mapper module: remove the foreign Mapper 38/Bit Corp comment block, restore the
missing newline after the `m034_bnrom_nina001.rs` module documentation, and
remove the duplicated `#[cfg(test)]` attribute while preserving the actual
mapper implementation and test module.

In `@crates/rustynes-mappers/src/m085_vrc7.rs`:
- Around line 535-632: Update M085 save_state and load_state to persist the 8
KiB prg_ram after the existing VRAM data, bump the state version, and adjust
size validation and offsets accordingly. Preserve compatibility expectations for
the new version while rejecting unsupported versions. Add a round-trip test
covering a write to $6000 and verifying the value after save/load.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs`:
- Line 19: The module-level and test-module Clippy allowances in m087_jaleco87
must be justified or narrowed. Add concise comments explaining the invariant and
necessity for each allowed lint, or remove the allowances by eliminating the
triggering casts and documentation patterns; apply the same treatment to both
visible allow attributes.
- Around line 235-246: Update the save_state_round_trip test for Jaleco87 to
write a distinctive nametable byte through the first mapper instance before
save_state, then assert the same byte at the corresponding PPU address after
load_state into the second instance. Keep the existing CHR-bank assertion and
use the existing cpu_write/ppu_read APIs.
- Around line 61-65: Update the CHR-ROM validation in the mapper initialization
path to reject sizes greater than 4 * CHR_BANK_8K, alongside the existing empty
and 8 KiB alignment checks. Preserve the current Invalid error behavior and
include the oversized length in the validation message; do not alter bank
selection logic.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs`:
- Around line 149-158: Update the cpu_read method’s $C000-$DFFF mapping to use
saturating subtraction when deriving the preceding PRG bank from last. Preserve
the existing bank selection for multi-bank ROMs while ensuring single-bank
images resolve safely without underflow or panic.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs`:
- Around line 40-47: Replace every stray mapper banner with documentation
matching the module’s actual mapper, or remove it when no accurate banner is
available. Apply this to the primary and secondary banners in
crates/rustynes-mappers/src/m107_magic_dragon107.rs (40-47 and 168-175),
m113_ave_nina006.rs (48-54 and 208-217), m132_txc_22211.rs (58-70 and 268-275),
m136_sachen_3011.rs (306-315), m156_daou156.rs (42-58 and 229-244),
m176_bmc_fk23c.rs (541-549), m177_hengedianzi.rs (52-59 and 182-188),
m179_hengedianzi.rs (53-60 and 183-193), and m180_nichibutsu180.rs (42-58 and
215-229); ensure no unrelated mapper names remain in these modules.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs`:
- Around line 206-209: Update the 0x8000..=0xFFFF PRG read branch to wrap the
computed offset by self.prg_rom.len() rather than count * PRG_BANK_32K, ensuring
all PRG sizes accepted by check_prg remain in bounds. Preserve the existing
bank-count logic only if still needed, and keep PRG_BANK_32K usage consistent
with the chosen fix.

In `@crates/rustynes-mappers/src/m185_cnrom185.rs`:
- Around line 46-62: Remove the orphaned mapper banner comments that document
implementations different from the surrounding types. Delete the specified
blocks in crates/rustynes-mappers/src/m185_cnrom185.rs (both sites),
m232_camerica_bf9096.rs (both sites), m240_cne_multicart.rs (both sites),
m241_bxrom241.rs, m244_cne_decathlon.rs (both sites), m246_fong_shen_bang246.rs,
m250_nitra250.rs, m268_bmc_coolboy.rs, and m513_sachen_9602.rs; preserve the
actual mapper implementations and relevant comments.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs`:
- Around line 185-186: Remove one duplicate #[cfg(test)] attribute above mod
tests in crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210, leaving a single
attribute on each test module.
- Line 5: Fix the malformed module-documentation line in
crates/rustynes-mappers/src/m240_cne_multicart.rs at lines 5-5 by separating the
prose ending in “may also be using.” from the trailing blank `//!` marker. Apply
the same documentation-only correction in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 5-5, separating the
prose ending in “the bank number directly.” from its bare `//!` line.

In `@crates/rustynes-mappers/src/m250_nitra250.rs`:
- Around line 128-135: Replace the unchecked `last - 1` arm in the PRG address
match with saturating subtraction, matching the behavior used by `m004_mmc3` and
`m268_bmc_coolboy`. Preserve the existing bank-selection logic while ensuring
single-bank PRG images resolve to bank 0 consistently in debug and release
builds.

In `@crates/rustynes-mappers/src/multicart_discrete.rs`:
- Around line 295-301: Replace the stale mapper header blocks with comments
describing the structs they immediately precede. In
crates/rustynes-mappers/src/multicart_discrete.rs:295-301, 595-606, and
2223-2232, retitle the headers for Multicart61, Multicart200, and Maxi15M234; in
crates/rustynes-mappers/src/ntdec.rs:246-260, 60-78, 411-422, 727-741, and
1123-1131, retitle them for Ntdec174, Ntdec63, Ntdec2722M40, NtdecAsder112, and
NtdecN625092; likewise update the headers at
crates/rustynes-mappers/src/sachen_8259.rs:58-74 and 248-261 for Sachen8259M137
and Sachen8259Variant, crates/rustynes-mappers/src/sachen_discrete.rs:62-74 and
1273-1282 for Sachen133 and SachenTca01M143, and
crates/rustynes-mappers/src/waixing.rs:63-81, 270-284, and 647-656 for
Waixing242, WaixingFs304M162, and Waixing253.
- Around line 3316-3320: Add board-specific minimum PRG ROM length validation in
the constructors for the multicart_discrete prg_32k path and bmc_simple 16 KiB
bank path, requiring at least PRG_BANK_32K and PRG_BANK_16K respectively before
construction succeeds. Keep the existing check_prg validation, but ensure
undersized ROMs are rejected rather than allowing the corresponding PRG read
helpers to panic; apply this in
crates/rustynes-mappers/src/multicart_discrete.rs lines 3316-3320 and
crates/rustynes-mappers/src/bmc_simple.rs lines 265-269.

In `@crates/rustynes-test-harness/tests/fast_dotloop_diff.rs`:
- Around line 246-366: Adjust vblank_io_torture_rom so its timing deliberately
places the $2006 and $2007 accesses within genuine idle-line spillover windows,
rather than relying on the current burst sequence to reach them. Trim the loop
or add a focused boundary-case ROM, and ensure the test still drives those
writes during vblank without spilling into the next frame.

In `@docs/expansion-audio.md`:
- Line 19: Synchronize the expansion-audio documentation with the i32 mixer
contract: in docs/expansion-audio.md at line 19, update the hook signature and
replace the deferred Sunsoft 5B level narrative with the calibrated v2.2.3
level_db_5b status; in docs/apu-2a03.md at lines 329-332, change the
expansion-audio introduction immediately above the table from i16 to i32.

In `@docs/mappers.md`:
- Around line 260-262: Update the per-board module list in the mapper
documentation to replace ave_nina.rs and cne240.rs with m079_ave_nina03_06.rs,
m113_ave_nina006.rs, and m240_cne_multicart.rs, while preserving the other
module entries and Tier-1 Curated statement.

In `@docs/performance.md`:
- Around line 1049-1051: Update the release tag example near the release flow
documentation to use v2.2.3 consistently, replacing the outdated v1.2.0 tag in
both the local tag creation and push command.

In `@docs/ppu-2c02.md`:
- Around line 242-265: Update the “Save-state coverage (PPU_SNAPSHOT_VERSION
v8)” section to state that pre-v8 .rns save states are rejected after the schema
version bump and cannot be automatically migrated. Clearly identify the upgrade
impact for users while preserving the existing description of the serialized PPU
fields and compatibility contract.

In `@scripts/bench_relative_check.sh`:
- Line 49: Update the BASE_REF initialization and surrounding flow in
bench_relative_check.sh so an explicitly empty BENCH_BASE_REF is treated as no
baseline rather than falling back to HEAD~1. When no baseline ref is provided,
exit successfully with status 0; preserve the existing default and relative
benchmark behavior for valid baseline refs.

In `@scripts/diag/dma_loop_span_analyzer.py`:
- Around line 5-11: Replace the bare exception handling in the row parsers with
explicit ValueError handling and report each malformed skipped row. Apply this
to the parsing loop in scripts/diag/dma_loop_span_analyzer.py (lines 5-11) and
the corresponding parser in scripts/diag/dma_sweep_analyzer.py (lines 5-10),
while preserving valid-row processing and the existing skip behavior.

In `@scripts/diag/dma_sweep_analyzer.py`:
- Around line 23-33: Update both previous-cycle checks in the halt display loop
and period-distribution loop to test explicitly for None rather than relying on
truthiness. Preserve cycle 0 as a valid previous value so subsequent deltas and
histogram entries are calculated correctly.

In `@scripts/diag/locate_first_trace_divergence.py`:
- Around line 9-28: Validate that both input and output paths are provided
before accessing sys.argv in the script’s main flow. Add an explicit usage check
or argparse-based argument parser, and emit a clear error message describing the
required input and output arguments instead of allowing an IndexError.

In `@scripts/diag/ppu2002_prerender_vbl_reads.py`:
- Around line 21-24: Update the mark comparison logic in the prerender read
analysis to compare each row’s PPU status value as well as its scanline and dot
coordinates. Include both left and right row value fields (index 4) so
equal-position rows with differing $2002 values are marked as divergences, while
preserving existing coordinate comparison behavior.

In `@scripts/diag/ppu2002_read_value_histogram.py`:
- Around line 3-9: Update the trace-reading loop around the file header and
integer parsing to handle empty and malformed input explicitly. Track the
current line number, report /tmp/RustyNES/s2002.csv and that line when the
header or required fields are invalid, or deliberately skip invalid rows with
clear handling; preserve processing of valid histogram rows and avoid exposing
raw StopIteration or ValueError tracebacks.
- Line 3: Update the histogram script’s file handling around the hard-coded
input and output paths: accept the input and output locations as command-line
arguments, avoid fixed /tmp names by creating or using a per-run directory, and
create the output exclusively or atomically to prevent collisions and symlink
replacement. Apply the same path-safety changes to both referenced file
operations.
- Around line 27-36: Strengthen the window predicate in the four-read scan
around masks, sls, and dots so matches require the intended consecutive-dot
sequence, prerender scanline boundary, and CPU-gap constraints before appending
output. Preserve the existing masked transition checks, and only revise the
comments if the implementation intentionally retains the weaker match.

In `@scripts/diag/ppu2004_diff_vs_answerkey.py`:
- Around line 42-48: Require complete, in-range dot coverage before reporting
diagnostics: in scripts/diag/ppu2004_diff_vs_answerkey.py lines 42-48 and
scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py lines 42-48, validate
exactly dots 0 through 340, reject missing or out-of-range entries with a clear
error, and count only valid dots; in
scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py lines 20-23, require
complete coverage from both Mesen and RustyNES before generating either
comparison.

In `@scripts/diag/ppu2007_fetch_bus_dump.py`:
- Around line 39-44: Align the reconstructed fetch_bus output range by updating
the banner and loop in the `fb` reporting block so they describe and iterate
over the same endpoints; preserve the existing formatting and choose either dots
0..39 or inclusive dots 0..40.

In `@scripts/diag/ppu2007_offset_greedy_search.py`:
- Around line 5-17: Update run to validate the scan_dma_abort subprocess before
parsing: use a bounded timeout, check the completed process return code, and
raise a clear error that includes relevant stderr on failure. Locate STRESS2007=
safely without indexing an empty match list, report missing or malformed output
clearly, and verify the decoded payload is long enough for the range(1,341,2)
scoring before iterating.

In `@scripts/diag/ppu2007_phase_offset_ranking.py`:
- Around line 25-28: Update the phase-5 diagnostic loop around val so
out-of-range samples such as val(d, -1) returning None are handled before
applying hexadecimal formatting. Preserve the existing formatted output for
valid values while rendering an explicit safe placeholder for missing samples,
preventing the debug section from raising TypeError.

In `@scripts/mapper-promotion/batch2.py`:
- Line 79: The generated Rust outputs use nonexistent hardcoded session-specific
paths. Update the output writes in scripts/mapper-promotion/batch2.py lines
79-79 and scripts/mapper-promotion/gen_promotion.py lines 57-57 to target a real
created directory, such as each script’s directory via __file__, while
preserving the respective promo_tests_2.rs and promo_tests.rs filenames.
- Around line 62-63: Guard the result of zip_nes(src) before passing it to
ines_mapper in the batch loop. When zip_nes returns None, append a per-item
error identifying the missing NES payload and continue to the next item,
matching the existing MISSING SRC handling; only call ines_mapper for a valid
payload.

In `@scripts/mapper-promotion/gen_promotion.py`:
- Around line 34-40: Update the promotion lookup around dirs[mid] and roms[0] to
handle missing staged directories and empty ROM lists without raising KeyError
or IndexError. Use the existing enumerate_staged.py pattern of safe directory
lookup and missing-item tracking, and emit a clear diagnostic for each
unavailable promotion id while allowing generation to continue for valid ids.

In `@scripts/pr-review/list_unresolved_threads.py`:
- Around line 5-7: Update the unresolved-thread output around the comments node
extraction to handle empty comments safely, using the guarded extraction pattern
from list_all_threads.py and skipping or safely handling threads without a first
comment. Replace direct databaseId and author dereferences with
c.get("databaseId") and a fallback author value when c.get("author") is null or
unavailable.

In `@scripts/pr-review/README.md`:
- Around line 15-22: Update the README’s reviewThreads query to paginate through
every page using the GraphQL cursor, merging all thread nodes before piping the
complete response to list_unresolved_threads.py or list_all_threads.py. Preserve
the existing thread fields and ensure the audit does not stop after the first
100 results.

In `@scripts/release-automation/split_commit.sh`:
- Line 8: Update the git add command in the release automation script to stage
the deleted legacy tqrom.rs and mmc3.rs paths alongside the new mapper files.
Preserve staging of the existing modified files so the reset/restore flow
records both renames or deletions correctly.

In `@tests/roms/mmc5/README.md`:
- Around line 32-35: Update the README reference to use the current rustynes
workspace names: replace the stale crates/nes-test-harness path with
crates/rustynes-test-harness and rename the nes-mappers crate reference to
rustynes-mappers, leaving the described test behavior unchanged.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`:
- Line 359: Update the mapper-module references in both documentation sites: in
to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md at lines 359-359, replace the
remaining sprintN.rs reference with the current board-numbered module layout; in
to-dos/ROADMAP.md at lines 340-340, rename mmc2_mmc4.rs to m010_mmc4.rs.

---

Outside diff comments:
In `@crates/rustynes-apu/src/snapshot.rs`:
- Around line 637-742: Add an end-of-input validation at the end of
Apu::restore, after the version-gated reset_4017_delay and reset_4017_value
fields are decoded. Reject any remaining bytes using the same error behavior as
Ppu::restore, while preserving successful restoration when r.pos equals
data.len().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e46ff3a2-3256-406e-ac2e-3ea71cd64fbf

📥 Commits

Reviewing files that changed from the base of the PR and between af47fe6 and 03c3956.

⛔ Files ignored due to path filters (33)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snap is excluded by !**/*.snap
📒 Files selected for processing (212)
  • .github/actionlint.yaml
  • .github/release-notes/v2.2.3.md
  • .github/workflows/ci.yml
  • .github/workflows/ios.yml
  • .github/workflows/pgo.yml
  • .github/workflows/release.yml
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/rustynes-apu/src/snapshot.rs
  • crates/rustynes-core/Cargo.toml
  • crates/rustynes-core/benches/full_frame.rs
  • crates/rustynes-core/benches/snapshot_restore.rs
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
  • crates/rustynes-core/src/nes.rs
  • crates/rustynes-frontend/src/app.rs
  • crates/rustynes-frontend/src/config.rs
  • crates/rustynes-frontend/src/debugger/settings_panel.rs
  • crates/rustynes-frontend/src/history_viewer.rs
  • crates/rustynes-frontend/src/runahead.rs
  • crates/rustynes-mappers/src/bmc_simple.rs
  • crates/rustynes-mappers/src/fds.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/kaiser.rs
  • crates/rustynes-mappers/src/lib.rs
  • crates/rustynes-mappers/src/m000_nrom.rs
  • crates/rustynes-mappers/src/m001_mmc1.rs
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m003_cnrom.rs
  • crates/rustynes-mappers/src/m004_mmc3.rs
  • crates/rustynes-mappers/src/m005_mmc5.rs
  • crates/rustynes-mappers/src/m007_axrom.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m010_mmc4.rs
  • crates/rustynes-mappers/src/m011_color_dreams.rs
  • crates/rustynes-mappers/src/m013_cprom.rs
  • crates/rustynes-mappers/src/m016_bandai_fcg.rs
  • crates/rustynes-mappers/src/m018_jaleco_ss88006.rs
  • crates/rustynes-mappers/src/m019_namco163.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m024_vrc6.rs
  • crates/rustynes-mappers/src/m032_irem_g101.rs
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs
  • crates/rustynes-mappers/src/m034_bnrom_nina001.rs
  • crates/rustynes-mappers/src/m035_jy_asic.rs
  • crates/rustynes-mappers/src/m036_txc_policeman.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs
  • crates/rustynes-mappers/src/m064_rambo1.rs
  • crates/rustynes-mappers/src/m065_irem_h3001.rs
  • crates/rustynes-mappers/src/m066_gxrom.rs
  • crates/rustynes-mappers/src/m067_sunsoft3.rs
  • crates/rustynes-mappers/src/m068_sunsoft4.rs
  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs
  • crates/rustynes-mappers/src/m070_bandai74.rs
  • crates/rustynes-mappers/src/m071_camerica_bf9093.rs
  • crates/rustynes-mappers/src/m073_vrc3.rs
  • crates/rustynes-mappers/src/m075_vrc1.rs
  • crates/rustynes-mappers/src/m076_namcot3446.rs
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs
  • crates/rustynes-mappers/src/m078_irem_jaleco78.rs
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs
  • crates/rustynes-mappers/src/m080_taito_x1_005.rs
  • crates/rustynes-mappers/src/m082_taito_x1_017.rs
  • crates/rustynes-mappers/src/m085_vrc7.rs
  • crates/rustynes-mappers/src/m087_jaleco87.rs
  • crates/rustynes-mappers/src/m088_namco118.rs
  • crates/rustynes-mappers/src/m089_sunsoft2.rs
  • crates/rustynes-mappers/src/m093_sunsoft3r.rs
  • crates/rustynes-mappers/src/m094_un1rom.rs
  • crates/rustynes-mappers/src/m095_namcot3425.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs
  • crates/rustynes-mappers/src/m099_vs_system.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/m113_ave_nina006.rs
  • crates/rustynes-mappers/src/m118_txsrom.rs
  • crates/rustynes-mappers/src/m119_tqrom.rs
  • crates/rustynes-mappers/src/m132_txc_22211.rs
  • crates/rustynes-mappers/src/m136_sachen_3011.rs
  • crates/rustynes-mappers/src/m151_konami_vs.rs
  • crates/rustynes-mappers/src/m152_bandai152.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs
  • crates/rustynes-mappers/src/m177_hengedianzi.rs
  • crates/rustynes-mappers/src/m179_hengedianzi.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/m184_sunsoft1.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m210_namco175.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m240_cne_multicart.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m250_nitra250.rs
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs
  • crates/rustynes-mappers/src/m513_sachen_9602.rs
  • crates/rustynes-mappers/src/mapper.rs
  • crates/rustynes-mappers/src/mmc3_clones.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs
  • crates/rustynes-mappers/src/nsf.rs
  • crates/rustynes-mappers/src/nsf_expansion.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/sprint10.rs
  • crates/rustynes-mappers/src/sprint11.rs
  • crates/rustynes-mappers/src/sprint12.rs
  • crates/rustynes-mappers/src/sprint13.rs
  • crates/rustynes-mappers/src/sprint2.rs
  • crates/rustynes-mappers/src/sprint3.rs
  • crates/rustynes-mappers/src/sprint5.rs
  • crates/rustynes-mappers/src/sprint6.rs
  • crates/rustynes-mappers/src/sprint7.rs
  • crates/rustynes-mappers/src/sprint8.rs
  • crates/rustynes-mappers/src/sprint9.rs
  • crates/rustynes-mappers/src/tier.rs
  • crates/rustynes-mappers/src/unif.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-mappers/tests/corpus.rs
  • crates/rustynes-netplay/tests/determinism.rs
  • crates/rustynes-netplay/tests/udp_loopback.rs
  • crates/rustynes-ppu/Cargo.toml
  • crates/rustynes-ppu/src/ppu.rs
  • crates/rustynes-ppu/src/snapshot.rs
  • crates/rustynes-test-harness/Cargo.toml
  • crates/rustynes-test-harness/src/bin/pgo_trainer.rs
  • crates/rustynes-test-harness/tests/accuracycoin_runahead.rs
  • crates/rustynes-test-harness/tests/audio_expansion.rs
  • crates/rustynes-test-harness/tests/cpu_reset.rs
  • crates/rustynes-test-harness/tests/fast_dotloop_diff.rs
  • crates/rustynes-test-harness/tests/holy_mapperel.rs
  • crates/rustynes-test-harness/tests/mmc3_clone_a12.rs
  • crates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rs
  • crates/rustynes-test-harness/tests/movie.rs
  • crates/rustynes-test-harness/tests/ppu_sprites.rs
  • crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
  • crates/rustynes-test-harness/tests/visual_regression.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md
  • docs/apu-2a03.md
  • docs/compatibility.md
  • docs/expansion-audio.md
  • docs/frontend.md
  • docs/mappers.md
  • docs/performance.md
  • docs/ppu-2c02.md
  • docs/testing-strategy.md
  • scripts/agy-review.sh
  • scripts/bench_relative_check.sh
  • scripts/diag/README.md
  • scripts/diag/dma_loop_span_analyzer.py
  • scripts/diag/dma_sweep_analyzer.py
  • scripts/diag/dump_trace_csv.py
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/diag/ppu2002_diff_traces.py
  • scripts/diag/ppu2002_isolated_exact_timed_reads.py
  • scripts/diag/ppu2002_prerender_vbl_reads.py
  • scripts/diag/ppu2002_read_context_window.py
  • scripts/diag/ppu2002_read_value_histogram.py
  • scripts/diag/ppu2004_diff_vs_answerkey.py
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py
  • scripts/diag/ppu2007_fetch_bus_dump.py
  • scripts/diag/ppu2007_key_alignment_shift_scan.py
  • scripts/diag/ppu2007_offset_greedy_search.py
  • scripts/diag/ppu2007_pattern_value_sets.py
  • scripts/diag/ppu2007_phase_offset_ranking.py
  • scripts/diag/ppu2007_stress_per_index_evaluator.py
  • scripts/mapper-promotion/README.md
  • scripts/mapper-promotion/batch2.py
  • scripts/mapper-promotion/convert_gg.py
  • scripts/mapper-promotion/enumerate_staged.py
  • scripts/mapper-promotion/gen_promotion.py
  • scripts/mapper-promotion/mapper_scan.py
  • scripts/mapper-promotion/promo_tests.rs
  • scripts/mapper-promotion/promo_tests_2.rs
  • scripts/mapper-promotion/scan.py
  • scripts/perf/perf_capture.sh
  • scripts/pr-review/README.md
  • scripts/pr-review/list_all_threads.py
  • scripts/pr-review/list_unresolved_threads.py
  • scripts/release-automation/split_commit.sh
  • tests/roms/LICENSES.md
  • tests/roms/README.md
  • tests/roms/assorted/README.md
  • tests/roms/assorted/apu_01_len_ctr.nes
  • tests/roms/assorted/apu_02_len_table.nes
  • tests/roms/assorted/branch_timing_1_basics.nes
  • tests/roms/assorted/branch_timing_2_backward.nes
  • tests/roms/assorted/branch_timing_3_forward.nes
  • tests/roms/assorted/cpu_reset_ram_after_reset.nes
  • tests/roms/assorted/cpu_reset_registers.nes
  • tests/roms/assorted/cpu_timing_test.nes
  • tests/roms/assorted/flowing_palette.nes
  • tests/roms/assorted/full_palette.nes
  • tests/roms/assorted/nestest.nes
  • tests/roms/assorted/oam_read.nes
  • tests/roms/assorted/oam_stress.nes
  • tests/roms/mmc5/README.md
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md
  • to-dos/ROADMAP.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (55)
.github/release-notes/v2.2.3.md (3)

19-21: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the actual config-default mechanism.

These lines attribute the migration behavior to #[serde(default)], while CHANGELOG.md lines 355-360 document default_fast_dotloop() instead. Use the implementation’s actual mechanism; this is part of the configuration compatibility contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 19 - 21, Update the release
note’s explanation of the fast_dotloop configuration migration to identify
default_fast_dotloop() as the mechanism that enables the option for existing
configs, replacing the inaccurate #[serde(default)] attribution. Preserve the
statements about the [emulation] config key and inherited speedup.

34-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the release test totals across both documents.

The release materials use both 2,219 and 2,237 for apparently overlapping --features test-roms verification. Clarify the subset boundaries and exact commands, or make the figures consistent.

  • .github/release-notes/v2.2.3.md#L34-L40: identify 2,219 as a specific subset or update it to the full 2,237 total.
  • CHANGELOG.md#L34-L40: apply the same scope and command clarification.
📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L34-L40 (this comment)
  • CHANGELOG.md#L34-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 34 - 40, The release test
totals need consistent scope and command descriptions: update the verification
references in .github/release-notes/v2.2.3.md lines 34-40 and CHANGELOG.md lines
34-40 to either identify 2,219 as a specific subset of the --features test-roms
tests or replace it with the full 2,237 total, applying the same clarification
and figures in both files.

105-107: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the shared elapsed-day claim.

The release date is July 23, 2026, and June 13, 2026 is 40 days earlier, not 28.

  • .github/release-notes/v2.2.3.md#L105-L107: correct the “28 days earlier” statement.
  • CHANGELOG.md#L81-L87: correct the interval or the blessing date.
📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L105-L107 (this comment)
  • CHANGELOG.md#L81-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 105 - 107, The shared
elapsed-day claim is incorrect: June 13, 2026 was 40 days before the July 23,
2026 release. Update the “28 days earlier” statement in
.github/release-notes/v2.2.3.md (lines 105-107) and correct the corresponding
interval or blessing date in CHANGELOG.md (lines 81-87) so both references are
consistent.
AGENTS.md (1)

188-188: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Finish updating the current-release chain.

This line still calls v2.2.2 “this release,” although the file identifies v2.2.3 as current. Reword it as the prior v2.2.2 release and remove the stale current-release wording.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 188, Update the current-release history sentence in
AGENTS.md so v2.2.2 “Conduit” is described as the prior release, not “this
release.” Remove the stale current-release wording while preserving the existing
v2.2.3 current-release description and historical chain.
crates/rustynes-core/src/bus.rs (2)

1631-1654: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the misplaced port API documentation.

Lines 1631-1637 document a port-taking, panicking API, but attach to set_zapper_temporal_light(bool). The generated public contract is false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/bus.rs` around lines 1631 - 1654, Remove the
misplaced port/overlay API documentation immediately before
set_zapper_temporal_light, including its port range and panic claims; retain
only documentation describing the boolean temporal-light behavior and its
default.

Source: Coding guidelines


2499-2508: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not map pre-render scanline -1 to visible scanline 0.

Line 2506 converts a negative PPU pre-render scanline to 0. A Zapper aimed at row 0 can therefore report light during pre-render, adding a bogus hold window. Return the drained/dark temporal value for negative scanlines and add a regression test.

Proposed fix
-            let sl = u16::try_from(self.ppu.scanline()).unwrap_or(0);
+            let Ok(sl) = u16::try_from(self.ppu.scanline()) else {
+                return z.read_at_scanline(self.ppu.framebuffer(), u16::MAX);
+            };
             return z.read_at_scanline(self.ppu.framebuffer(), sl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        // A3 (v2.2.3, opt-in): serve the Zapper's light bit from the
        // beam-relative model. `read_at_scanline` takes `&self` and the PPU is a
        // different field, so these are disjoint borrows. Off by default, so the
        // shipped path below is byte-identical.
        if self.zapper_temporal_light
            && let Some(crate::input_device::InputDevice::Zapper(z)) = &self.expansion_device[port]
        {
            let Ok(sl) = u16::try_from(self.ppu.scanline()) else {
                return z.read_at_scanline(self.ppu.framebuffer(), u16::MAX);
            };
            return z.read_at_scanline(self.ppu.framebuffer(), sl);
        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/bus.rs` around lines 2499 - 2508, Update the zapper
temporal-light branch in the bus read path to detect the PPU’s negative
pre-render scanline before converting it to u16, and return the drained/dark
temporal value instead of sampling framebuffer row 0. Preserve normal
beam-relative sampling for nonnegative scanlines, and add a regression test
covering a Zapper aimed at row 0 during pre-render.
crates/rustynes-mappers/src/m079_ave_nina03_06.rs (1)

53-59: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale template comment describes the wrong mapper. Lines 53-59 document "Mapper 38 — Bit Corp UNL-PCI556", which has nothing to do with the AVE NINA-03/06 (mapper 79) board this file implements. Line 11 also lost a newline (...m034_bnrom_nina001.rs.//!renders the//!literally), and#[cfg(test)]is duplicated at lines 226-227. Same copy-paste residue asm076`; remove the foreign block, fix the line-11 break, and drop the duplicate attribute.

Also applies to: 226-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m079_ave_nina03_06.rs` around lines 53 - 59,
Clean up the copied template residue in the AVE NINA-03/06 mapper module: remove
the foreign Mapper 38/Bit Corp comment block, restore the missing newline after
the `m034_bnrom_nina001.rs` module documentation, and remove the duplicated
`#[cfg(test)]` attribute while preserving the actual mapper implementation and
test module.
crates/rustynes-mappers/src/m107_magic_dragon107.rs (1)

40-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stray banner comments describe the wrong mapper. The module rename copied template banner blocks that document mappers unrelated to each file, so the in-file "spec" comments now contradict the actual implementation. Remove or replace each with a banner matching the module's real mapper.

  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47: "Mapper 31 — INL / NSF-style" banner in the Magic Dragon (107) module; also the "Mapper 143 — Sachen TCA01" block at L168-175.
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54: "Mapper 38 — Bit Corp" banner in the NINA-006 (113) module; also "Mapper 86 — Jaleco JF-13" at L208-217.
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70: "Mapper 15 — K-1029" banner in the TXC 22211 (132) module; also "Mapper 133 — Sachen 3009" at L268-275.
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315: "SimpleBmc" banner describing unrelated boards (164/261/…) in the Sachen 3011 (136) module.
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58: "Mapper 40 — NTDEC 2722" banner in the Daou (156) module; also "Mapper 162 — Waixing FS304" at L229-244.
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549: "Coolboy (mapper 268)" banner in the FK23C (176) module.
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59: "Mapper 31 — INL" banner in the Hengedianzi (177) module; also "Mapper 179" block at L182-188.
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60: "Mapper 31 — INL" banner in the Hengedianzi (179) module; also "Mapper 58 — multicart" at L183-193.
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58: "Mapper 147 — Sachen 3018" banner in the Nichibutsu (180) module; also "Mapper 185" block at L215-229.
📍 Affects 9 files
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47 (this comment)
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs` around lines 40 - 47,
Replace every stray mapper banner with documentation matching the module’s
actual mapper, or remove it when no accurate banner is available. Apply this to
the primary and secondary banners in
crates/rustynes-mappers/src/m107_magic_dragon107.rs (40-47 and 168-175),
m113_ave_nina006.rs (48-54 and 208-217), m132_txc_22211.rs (58-70 and 268-275),
m136_sachen_3011.rs (306-315), m156_daou156.rs (42-58 and 229-244),
m176_bmc_fk23c.rs (541-549), m177_hengedianzi.rs (52-59 and 182-188),
m179_hengedianzi.rs (53-60 and 183-193), and m180_nichibutsu180.rs (42-58 and
215-229); ensure no unrelated mapper names remain in these modules.
crates/rustynes-mappers/src/m136_sachen_3011.rs (1)

206-209: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Out-of-bounds panic for sub-32 KiB PRG. check_prg (Line 79) only requires an 8 KiB multiple, but this read assumes a 32 KiB image. For an 8/16/24 KiB PRG, count = len / PRG_BANK_32K = 0 → max(1) = 1, so % (count * PRG_BANK_32K) is % 0x8000 — a no-op. addr & 0x7FFF then reaches 0x7FFC on the reset-vector fetch ($FFFC), indexing past a slice that is only 0x2000/0x4000/0x6000 bytes long, panicking on the first CPU read. This contradicts the module's "cannot afford a panic on a register access" invariant and is reachable directly from a ROM-supplied PRG size.

Wrap by the actual slice length so any validated size is in-bounds:

Proposed fix
             0x8000..=0xFFFF => {
-                let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
-                self.prg_rom[(addr as usize & 0x7FFF) % (count * PRG_BANK_32K)]
+                self.prg_rom[(addr as usize & 0x7FFF) % self.prg_rom.len()]
             }

Alternatively, tighten check_prg for this board to reject non-32 KiB-multiple PRG. PRG_BANK_32K would then be unused here.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            0x8000..=0xFFFF => {
                self.prg_rom[(addr as usize & 0x7FFF) % self.prg_rom.len()]
            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs` around lines 206 - 209,
Update the 0x8000..=0xFFFF PRG read branch to wrap the computed offset by
self.prg_rom.len() rather than count * PRG_BANK_32K, ensuring all PRG sizes
accepted by check_prg remain in bounds. Preserve the existing bank-count logic
only if still needed, and keep PRG_BANK_32K usage consistent with the chosen
fix.
crates/rustynes-mappers/src/m185_cnrom185.rs (1)

46-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Orphaned section-divider comments describe mappers not implemented in these files. The module split left behind large banner comments documenting a different mapper than the file's actual type, which is misleading in a repo where docs are treated as spec. Delete (or relocate to the owning module) each stray block:

  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62: remove the "Mapper 147 — Sachen 3018 (TXC JV001)" banner (file implements CnRom185/185).
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L234-L245: remove the "Mapper 200 — MG109" banner.
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44: remove the "Mapper 38 — Bit Corp UNL-PCI556" banner (file implements Camerica232/232).
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L191-L200: remove the "Mapper 240 — C&E multicart" banner.
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53: remove the "Mapper 38" banner (file implements Cne240/240).
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183: remove the "Mapper 241 — BxROM-like pirate" banner.
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41: remove the "Mapper 38" banner (file implements Bxrom241/241).
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53: remove the "Mapper 38" banner (file implements Decathlon244/244).
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207: remove the "Mapper 250 — Nitra" banner.
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59: remove the "Mapper 28 — Action 53" banner (file implements FongShenBang246/246).
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58: remove the "Mapper 40 — NTDEC 2722" banner (file implements Nitra250/250).
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451: remove the "Sachen9602 (mapper 513)" banner (that board lives in m513_sachen_9602.rs).
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344: remove the "TxcChip / Sachen 3011 (m136)" banner.
📍 Affects 9 files
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62 (this comment)
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L234-L245
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L191-L200
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m185_cnrom185.rs` around lines 46 - 62, Remove
the orphaned mapper banner comments that document implementations different from
the surrounding types. Delete the specified blocks in
crates/rustynes-mappers/src/m185_cnrom185.rs (both sites),
m232_camerica_bf9096.rs (both sites), m240_cne_multicart.rs (both sites),
m241_bxrom241.rs, m244_cne_decathlon.rs (both sites), m246_fong_shen_bang246.rs,
m250_nitra250.rs, m268_bmc_coolboy.rs, and m513_sachen_9602.rs; preserve the
actual mapper implementations and relevant comments.
crates/rustynes-mappers/src/m240_cne_multicart.rs (2)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Malformed module-doc line: a blank doc marker was appended to prose. The trailing //! renders literally and suppresses the intended paragraph break before "A discrete-logic board...".

  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L5: split ...may also be using.//! into a ...may also be using. line followed by a bare //! line.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L5: split ...the bank number directly.//! into a ...directly. line followed by a bare //! line.
📍 Affects 2 files
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L5-L5 (this comment)
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L5-L5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs` at line 5, Fix the
malformed module-documentation line in
crates/rustynes-mappers/src/m240_cne_multicart.rs at lines 5-5 by separating the
prose ending in “may also be using.” from the trailing blank `//!` marker. Apply
the same documentation-only correction in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 5-5, separating the
prose ending in “the bank number directly.” from its bare `//!` line.

185-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate #[cfg(test)] attribute on the test module. Both files stack the attribute twice; the second is dead redundancy (both must hold, so behavior is unchanged). Drop the duplicate.

  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186: remove one of the two #[cfg(test)] lines above mod tests.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210: remove one of the two #[cfg(test)] lines above mod tests.
📍 Affects 2 files
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186 (this comment)
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs` around lines 185 - 186,
Remove one duplicate #[cfg(test)] attribute above mod tests in
crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210, leaving a single
attribute on each test module.
crates/rustynes-mappers/src/m250_nitra250.rs (1)

128-135: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

last - 1 underflows for a single-bank PRG image.

last is (len / 8K).max(1) - 1, i.e. 0 for an 8 KiB PRG. The (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1 arm then evaluates 0usize - 1: this panics in a debug build (overflow check) and wraps to usize::MAX in release (masked later by % count in read_prg), so the two build profiles diverge silently. new accepts any non-zero multiple of 8 KiB, so a crafted ROM header reaches this on a register-driven read. Use saturating_sub, matching m004_mmc3/m268_bmc_coolboy.

Proposed fix
-            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1,
+            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last.saturating_sub(1),
As per coding guidelines: "Outside `#[cfg(test)]` code, do not add ... `panic!()` when applied to untrusted data parsed from ROMs ... Return a typed `Result` or error instead".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        let r6 = self.bank_regs[6] as usize;
        let r7 = self.bank_regs[7] as usize;
        match (self.prg_mode, addr) {
            (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last.saturating_sub(1),
            (_, 0xA000..=0xBFFF) => r7,
            _ => last,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m250_nitra250.rs` around lines 128 - 135, Replace
the unchecked `last - 1` arm in the PRG address match with saturating
subtraction, matching the behavior used by `m004_mmc3` and `m268_bmc_coolboy`.
Preserve the existing bank-selection logic while ensuring single-bank PRG images
resolve to bank 0 consistently in debug and release builds.

Source: Coding guidelines

crates/rustynes-mappers/src/multicart_discrete.rs (2)

295-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale header comment blocks describe mappers that are no longer in these files. The mapper module rename moved several mappers into their own mNNN_*.rs files, leaving orphaned === header comment blocks that now sit above unrelated structs. Each block documents register decode for a mapper the following code does not implement, which is actively misleading for maintenance. Remove or replace each with the header for the struct it precedes.

  • crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301: "Mapper 36 — TXC 01-22000 (Policeman)" precedes Multicart61 (mapper 61); retitle to mapper 61 (further instances at L595-L606 "Mapper 72" above Multicart200, and L2223-L2232 "Mapper 111" above Maxi15M234).
  • crates/rustynes-mappers/src/ntdec.rs#L246-L260: "Mapper 76 — NAMCOT-3446" precedes Ntdec174 (mapper 174); retitle (also L60-L78 "Mapper 28 Action 53" above Ntdec63, L411-L422 "Mapper 225" above Ntdec2722M40, L727-L741 "Mapper 95" above NtdecAsder112, L1123-L1131 "Bmc204" above NtdecN625092).
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74: "Mapper 40 — NTDEC 2722" precedes Sachen8259M137 (mapper 137); retitle (also L248-L261 "Mapper 156" above Sachen8259Variant).
  • crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74: "Mapper 15 — K-1029" precedes Sachen133 (mapper 133); retitle (also L1273-L1282 "Mapper 180" above SachenTca01M143).
  • crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedes Waixing242 (mapper 242); retitle (also L270-L284 "Mapper 246" above WaixingFs304M162, L647-L656 "Mapper 244" above Waixing253).
📍 Affects 5 files
  • crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301 (this comment)
  • crates/rustynes-mappers/src/ntdec.rs#L246-L260
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74
  • crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74
  • crates/rustynes-mappers/src/waixing.rs#L63-L81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/multicart_discrete.rs` around lines 295 - 301,
Replace the stale mapper header blocks with comments describing the structs they
immediately precede. In
crates/rustynes-mappers/src/multicart_discrete.rs:295-301, 595-606, and
2223-2232, retitle the headers for Multicart61, Multicart200, and Maxi15M234; in
crates/rustynes-mappers/src/ntdec.rs:246-260, 60-78, 411-422, 727-741, and
1123-1131, retitle them for Ntdec174, Ntdec63, Ntdec2722M40, NtdecAsder112, and
NtdecN625092; likewise update the headers at
crates/rustynes-mappers/src/sachen_8259.rs:58-74 and 248-261 for Sachen8259M137
and Sachen8259Variant, crates/rustynes-mappers/src/sachen_discrete.rs:62-74 and
1273-1282 for Sachen133 and SachenTca01M143, and
crates/rustynes-mappers/src/waixing.rs:63-81, 270-284, and 647-656 for
Waixing242, WaixingFs304M162, and Waixing253.

3316-3320: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether any upstream loader enforces a minimum PRG size before constructing these mappers.
rg -nP 'new_m(46|57|51|104|290|301|164|261|289|320|336|349|286)\s*\(' crates/rustynes-mappers/src crates/rustynes-core/src
rg -nP '(prg_rom|prg)\.len\(\)\s*[<>]=?\s*(PRG_BANK_16K|PRG_BANK_32K|0x[0-9A-Fa-f]+)' crates/rustynes-mappers/src
ast-grep run --pattern 'fn from_ines($$$) { $$$ }' --lang rust crates/rustynes-mappers/src 2>/dev/null | head -40

Repository: doublegate/RustyNES

Length of output: 4082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant constructors and size checks.
sed -n '1,220p' crates/rustynes-mappers/src/bmc_simple.rs
printf '\n--- multicart_discrete ---\n'
sed -n '3240,3345p' crates/rustynes-mappers/src/multicart_discrete.rs
printf '\n--- lib mapper dispatch around affected mappers ---\n'
sed -n '1240,1370p' crates/rustynes-mappers/src/lib.rs
printf '\n--- existing PRG validation sites in the same files ---\n'
sed -n '1120,1175p' crates/rustynes-mappers/src/ntdec.rs
sed -n '3640,3895p' crates/rustynes-mappers/src/multicart_discrete.rs

Repository: doublegate/RustyNES

Length of output: 25300


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find exact signatures and helper constants for the relevant index calculations.
rg -n "fn prg_byte|fn prg_32k|fn prg_16k|fn new_m164|fn new_m46|fn new_m57|fn new_m104|fn new_m290|fn new_m301|fn new_m261|fn new_m289|fn new_m320|fn new_m336|fn new_m349|fn new_m286|check_prg|PRG_BANK_16K|PRG_BANK_32K" crates/rustynes-mappers/src/bmc_simple.rs crates/rustynes-mappers/src/multicart_discrete.rs crates/rustynes-mappers/src/lib.rs

Repository: doublegate/RustyNES

Length of output: 12900


Guard these PRG reads with board-specific minimum sizes.

  • crates/rustynes-mappers/src/multicart_discrete.rs#L3316-L3320 and crates/rustynes-mappers/src/bmc_simple.rs#L265-L269: check_prg only enforces 8 KiB alignment, but these helpers index 32 KiB/16 KiB banks. A too-small ROM can still construct successfully and panic on the first CPU read. Add minimum-length checks in the relevant constructors (>= PRG_BANK_32K or >= PRG_BANK_16K, as appropriate) instead of relying on the shared 8 KiB check.
📍 Affects 2 files
  • crates/rustynes-mappers/src/multicart_discrete.rs#L3316-L3320 (this comment)
  • crates/rustynes-mappers/src/bmc_simple.rs#L265-L269
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/multicart_discrete.rs` around lines 3316 - 3320,
Add board-specific minimum PRG ROM length validation in the constructors for the
multicart_discrete prg_32k path and bmc_simple 16 KiB bank path, requiring at
least PRG_BANK_32K and PRG_BANK_16K respectively before construction succeeds.
Keep the existing check_prg validation, but ensure undersized ROMs are rejected
rather than allowing the corresponding PRG read helpers to panic; apply this in
crates/rustynes-mappers/src/multicart_discrete.rs lines 3316-3320 and
crates/rustynes-mappers/src/bmc_simple.rs lines 265-269.

Source: Coding guidelines

crates/rustynes-test-harness/tests/fast_dotloop_diff.rs (1)

246-366: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the exact COPY_V_DELAY / RENDER_BUFFER_DOT_DELAY constant values,
# which determine how many dots a spillover countdown can survive past a
# render-line boundary, to sanity-check the scanline-overrun math above.
rg -n 'const COPY_V_DELAY' crates/rustynes-ppu/src
rg -n 'RENDER_BUFFER_DOT_DELAY' crates/rustynes-ppu/src

Repository: doublegate/RustyNES

Length of output: 529


vblank_io_torture_rom still doesn’t structurally cover idle-line $2006/$2007 staging

COPY_V_DELAY is only 4 dots and the $2007 read buffer delay is 5, so the spillover window is very small; with a ~37-cycle burst loop, 100 iterations still run past vblank into the next frame. That means this ROM reliably exercises the $2001 idle-line arm, but it does not clearly force $2006/$2007 onto genuine idle-line dots. Trim the loop or add a boundary-case ROM that lands those writes on an idle line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-test-harness/tests/fast_dotloop_diff.rs` around lines 246 -
366, Adjust vblank_io_torture_rom so its timing deliberately places the $2006
and $2007 accesses within genuine idle-line spillover windows, rather than
relying on the current burst sequence to reach them. Trim the loop or add a
focused boundary-case ROM, and ensure the test still drives those writes during
vblank without spilling into the next frame.

Source: Path instructions

docs/expansion-audio.md (1)

19-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize expansion-audio documentation with the i32 mixer contract.

This PR widens Mapper::mix_audio to i32 and calibrates/asserts Sunsoft 5B, but both documents retain the old i16 contract.

  • docs/expansion-audio.md#L19-L19: update the hook signature and replace the deferred-5B-level narrative with the v2.2.3 calibrated level_db_5b status.
  • docs/apu-2a03.md#L329-L332: update the expansion-audio introduction immediately above this table from i16 to i32.

As per coding guidelines, “implementation specifications are not merely historical documentation.”

📍 Affects 2 files
  • docs/expansion-audio.md#L19-L19 (this comment)
  • docs/apu-2a03.md#L329-L332
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/expansion-audio.md` at line 19, Synchronize the expansion-audio
documentation with the i32 mixer contract: in docs/expansion-audio.md at line
19, update the hook signature and replace the deferred Sunsoft 5B level
narrative with the calibrated v2.2.3 level_db_5b status; in docs/apu-2a03.md at
lines 329-332, change the expansion-audio introduction immediately above the
table from i16 to i32.

Sources: Coding guidelines, Path instructions

docs/mappers.md (1)

260-262: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the renamed module list.

ave_nina.rs and cne240.rs do not match this PR’s board-named modules. List m079_ave_nina03_06.rs, m113_ave_nina006.rs, and m240_cne_multicart.rs instead.

As per path instructions, “Docs are the spec here, not a changelog. Flag documentation that drifts from the code it describes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mappers.md` around lines 260 - 262, Update the per-board module list in
the mapper documentation to replace ave_nina.rs and cne240.rs with
m079_ave_nina03_06.rs, m113_ave_nina006.rs, and m240_cne_multicart.rs, while
preserving the other module entries and Tier-1 Curated statement.

Source: Path instructions

docs/performance.md (1)

1049-1051: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the v2.2.3 release tag in this example.

The surrounding section documents the v2.2.3 release flow, but the command still pushes v1.2.0. Copying it would trigger the workflow with the wrong release version; use v2.2.3 or a clearly generic placeholder such as <version>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/performance.md` around lines 1049 - 1051, Update the release tag example
near the release flow documentation to use v2.2.3 consistently, replacing the
outdated v1.2.0 tag in both the local tag creation and push command.

Source: Path instructions

docs/ppu-2c02.md (1)

242-265: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the pre-v8 save-state incompatibility.

This section explains the new schema fields but does not state that existing pre-v8 .rns states are rejected after the PPU_SNAPSHOT_VERSION bump. Add the upgrade impact and make clear that no automatic migration is available, so users do not assume older states remain loadable.

As per path instructions, Markdown is the subsystem specification and must match the save-state compatibility contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ppu-2c02.md` around lines 242 - 265, Update the “Save-state coverage
(PPU_SNAPSHOT_VERSION v8)” section to state that pre-v8 .rns save states are
rejected after the schema version bump and cannot be automatically migrated.
Clearly identify the upgrade impact for users while preserving the existing
description of the serialized PPU fields and compatibility contract.

Source: Path instructions

scripts/bench_relative_check.sh (1)

49-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
BENCH_BASE_REF=''
base_ref="${1:-${BENCH_BASE_REF:-HEAD~1}}"
test "${base_ref}" = 'HEAD~1'

Repository: doublegate/RustyNES

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== scripts/bench_relative_check.sh ==\n'
wc -l scripts/bench_relative_check.sh
cat -n scripts/bench_relative_check.sh | sed -n '1,220p'

printf '\n== .github/workflows/ci.yml ==\n'
wc -l .github/workflows/ci.yml
cat -n .github/workflows/ci.yml | sed -n '1,260p'

Repository: doublegate/RustyNES

Length of output: 23689


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== search BENCH_BASE_REF =='
rg -n "BENCH_BASE_REF|bench_relative_check.sh|bench_regression_check.sh|relative gate|skip" .github/workflows/ci.yml scripts/bench_relative_check.sh

echo
echo '== bench-related workflow section =='
# Show the surrounding lines for the bench job invocation
grep -n -A40 -B20 "bench_relative_check.sh" .github/workflows/ci.yml

Repository: doublegate/RustyNES

Length of output: 5014


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== search BENCH_BASE_REF =='
rg -n "BENCH_BASE_REF|bench_relative_check.sh|bench_regression_check.sh|relative gate|skip" .github/workflows/ci.yml scripts/bench_relative_check.sh

echo
echo '== bench-related workflow section =='
grep -n -A40 -B20 "bench_relative_check.sh" .github/workflows/ci.yml

Repository: doublegate/RustyNES

Length of output: 5014


Skip an explicitly empty baseline ref.
scripts/bench_relative_check.sh:49 turns an empty BENCH_BASE_REF into HEAD~1, so the relative bench can run against the parent commit instead of skipping on workflow_dispatch / other no-base-ref paths. Treat empty as “no baseline” and exit 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench_relative_check.sh` at line 49, Update the BASE_REF
initialization and surrounding flow in bench_relative_check.sh so an explicitly
empty BENCH_BASE_REF is treated as no baseline rather than falling back to
HEAD~1. When no baseline ref is provided, exit successfully with status 0;
preserve the existing default and relative benchmark behavior for valid baseline
refs.
scripts/diag/dma_loop_span_analyzer.py (1)

5-11: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report malformed diagnostic input instead of silently biasing both analyses.

Both parsers use a bare except followed by continue, so malformed rows disappear from the computed statistics without warning.

  • scripts/diag/dma_loop_span_analyzer.py#L5-L11: catch ValueError explicitly and report the skipped row.
  • scripts/diag/dma_sweep_analyzer.py#L5-L10: apply the same explicit parse-error handling.
🧰 Tools
🪛 Ruff (0.15.21)

[error] 5-5: Ambiguous variable name: l

(E741)


[error] 7-7: Multiple statements on one line (colon)

(E701)


[error] 8-8: Multiple statements on one line (colon)

(E701)


[error] 8-8: Multiple statements on one line (semicolon)

(E702)


[error] 9-9: Do not use bare except

(E722)


[error] 9-9: try-except-continue detected, consider logging the exception

(S112)


[error] 9-9: Multiple statements on one line (colon)

(E701)

📍 Affects 2 files
  • scripts/diag/dma_loop_span_analyzer.py#L5-L11 (this comment)
  • scripts/diag/dma_sweep_analyzer.py#L5-L10
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/dma_loop_span_analyzer.py` around lines 5 - 11, Replace the bare
exception handling in the row parsers with explicit ValueError handling and
report each malformed skipped row. Apply this to the parsing loop in
scripts/diag/dma_loop_span_analyzer.py (lines 5-11) and the corresponding parser
in scripts/diag/dma_sweep_analyzer.py (lines 5-10), while preserving valid-row
processing and the existing skip behavior.

Sources: Path instructions, Linters/SAST tools

scripts/diag/dma_sweep_analyzer.py (1)

23-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an explicit None check for the previous cycle.

If the first valid halt occurs at cycle 0, both if prev checks treat it as uninitialized. The first displayed delta and the following halt-period histogram are then wrong.

Proposed fix
-    d = c-prev if prev else 0
+    d = c-prev if prev is not None else 0
...
-    if prev: deltas[c-prev]+=1
+    if prev is not None: deltas[c-prev]+=1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

prev=None
for c,h in halts[:40]:
    d = c-prev if prev is not None else 0
    prev=c
    print(f"  +{d:4d}  ${h:04X}")
# period distribution
deltas=collections.Counter()
prev=None
for c,h in halts:
    if prev is not None: deltas[c-prev]+=1
    prev=c
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 31-31: Loop control variable h not used within loop body

(B007)


[error] 32-32: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/dma_sweep_analyzer.py` around lines 23 - 33, Update both
previous-cycle checks in the halt display loop and period-distribution loop to
test explicitly for None rather than relying on truthiness. Preserve cycle 0 as
a valid previous value so subsequent deltas and histogram entries are calculated
correctly.
scripts/diag/locate_first_trace_divergence.py (1)

9-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate required CLI arguments.

Missing input or output paths raise an unhelpful IndexError. Use argparse or an explicit usage check before indexing sys.argv.

As per path instructions, “Prioritize correctness and clear error messages over style.”

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 27-27: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(sys.argv[2],'w')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 11-11: Use format specifiers instead of percent format

(UP031)


[warning] 23-23: Use format specifiers instead of percent format

(UP031)


[warning] 26-26: Use format specifiers instead of percent format

(UP031)


[warning] 28-28: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/locate_first_trace_divergence.py` around lines 9 - 28, Validate
that both input and output paths are provided before accessing sys.argv in the
script’s main flow. Add an explicit usage check or argparse-based argument
parser, and emit a clear error message describing the required input and output
arguments instead of allowing an IndexError.

Source: Path instructions

scripts/diag/ppu2002_prerender_vbl_reads.py (1)

21-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark value-only divergences too.

mark ignores val, so equal (scanline, dot) rows with different $2002 values appear unmarked. Include l[4] and r[4] in the comparison.

As per path instructions, “Prioritize correctness and clear error messages over style.”

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 22-22: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_prerender_vbl_reads.py` around lines 21 - 24, Update the
mark comparison logic in the prerender read analysis to compare each row’s PPU
status value as well as its scanline and dot coordinates. Include both left and
right row value fields (index 4) so equal-position rows with differing $2002
values are marked as divergences, while preserving existing coordinate
comparison behavior.

Source: Path instructions

scripts/diag/ppu2002_read_value_histogram.py (3)

3-3: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Security Misconfiguration (CWE-377): Insecure Temporary File

Reachability: Internal

Avoid predictable /tmp input and output paths.

Hard-coded paths make the script non-portable and allow concurrent runs to collide. On a shared host, another local process could replace the input or pre-create the output as a symlink. Accept paths as arguments and use a per-run directory with exclusive or atomic output creation.

Also applies to: 37-37

🧰 Tools
🪛 Ruff (0.15.21)

[error] 3-3: Probable insecure usage of temporary file or directory: "/tmp/RustyNES/s2002.csv"

(S108)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` at line 3, Update the histogram
script’s file handling around the hard-coded input and output paths: accept the
input and output locations as command-line arguments, avoid fixed /tmp names by
creating or using a per-run directory, and create the output exclusively or
atomically to prevent collisions and symlink replacement. Apply the same
path-safety changes to both referenced file operations.

Sources: Path instructions, Linters/SAST tools


3-9: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add line-aware handling for empty and malformed traces.

next(f) raises StopIteration for an empty file, while int(...) raises an uncontextualized ValueError for malformed fields. Report the input path and line number, or explicitly skip invalid rows; otherwise a bad capture produces a raw traceback that can be mistaken for an emulator failure.

🧰 Tools
🪛 Ruff (0.15.21)

[error] 3-3: Probable insecure usage of temporary file or directory: "/tmp/RustyNES/s2002.csv"

(S108)


[error] 7-7: Multiple statements on one line (colon)

(E701)


[error] 9-9: Multiple statements on one line (semicolon)

(E702)


[error] 9-9: Multiple statements on one line (semicolon)

(E702)


[error] 9-9: Multiple statements on one line (semicolon)

(E702)


[error] 9-9: Multiple statements on one line (semicolon)

(E702)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 3 - 9, Update the
trace-reading loop around the file header and integer parsing to handle empty
and malformed input explicitly. Track the current line number, report
/tmp/RustyNES/s2002.csv and that line when the header or required fields are
invalid, or deliberately skip invalid rows with clear handling; preserve
processing of valid histogram rows and avoid exposing raw StopIteration or
ValueError tracebacks.

Source: Path instructions


27-36: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Enforce the dot and scanline constraints before reporting matches.

The predicate checks only the masked values. It never uses the collected dots or sls, so unrelated four-row windows can be reported as the documented pattern. Add the intended consecutive-dot, prerender-boundary, and CPU-gap checks, or update the comments to describe the weaker match.

Proposed check
+    sls = [r[2] for r in w]
+    dots = [r[3] for r in w]
     if masks[0] == 0xE0 and masks[3] == 0x00 \
+        and all(sl in (260, 261, -1, 0) for sl in sls) \
+        and all(b == a + 1 for a, b in zip(dots, dots[1:])) \
         and masks[0] >= masks[1] >= masks[2] >= masks[3]:
-        sls=[r[2] for r in w]; dots=[r[3] for r in w]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

# Also: find the 4-read test pattern = consecutive reads (in trace order) where AND 0xE0
# transitions E0->E0->80->00 or similar. Scan windows of 4 with strictly stepping dot.
out.append("---scanning for E0/80/00 transition windows (masked 0xE0)---")
for i in range(len(rows)-3):
    w=rows[i:i+4]
    masks=[r[4]&0xE0 for r in w]
    sls = [r[2] for r in w]
    dots = [r[3] for r in w]
    # test signature: starts 0xE0, ends 0x00, monotonic non-increasing, on prerender-ish
    if masks[0] == 0xE0 and masks[3] == 0x00 \
        and all(sl in (260, 261, -1, 0) for sl in sls) \
        and all(b == a + 1 for a, b in zip(dots, dots[1:])) \
        and masks[0] >= masks[1] >= masks[2] >= masks[3]:
        out.append("  win@%d sls=%s dots=%s masks=%s"%(i,sls,dots,[hex(m) for m in masks]))
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 36-36: Do not hardcode temporary file or directory names
Context: '/tmp/RustyNES/an_out.txt'
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.15.21)

[error] 35-35: Multiple statements on one line (semicolon)

(E702)


[warning] 36-36: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 27 - 36,
Strengthen the window predicate in the four-read scan around masks, sls, and
dots so matches require the intended consecutive-dot sequence, prerender
scanline boundary, and CPU-gap constraints before appending output. Preserve the
existing masked transition checks, and only revise the comments if the
implementation intentionally retains the weaker match.

Source: Path instructions

scripts/diag/ppu2004_diff_vs_answerkey.py (1)

42-48: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail diagnostics on incomplete dot coverage.

Absent dots are skipped, so an empty or partial capture can report zero mismatches; out-of-range dots can also inflate the displayed capture count.

  • scripts/diag/ppu2004_diff_vs_answerkey.py#L42-L48: reject missing or out-of-range dots before reporting mismatches.
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py#L42-L48: apply the same full 0..=340 coverage check.
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py#L20-L23: require complete coverage from both Mesen and RustyNES before generating either comparison.

As per path instructions, “Prioritize correctness and clear error messages over style.”

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 44-44: Use dot2val.get(d) instead of dot2val.get(d, None)

Replace dot2val.get(d, None) with dot2val.get(d)

(SIM910)


[error] 45-45: Multiple statements on one line (colon)

(E701)

📍 Affects 3 files
  • scripts/diag/ppu2004_diff_vs_answerkey.py#L42-L48 (this comment)
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py#L42-L48
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py#L20-L23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2004_diff_vs_answerkey.py` around lines 42 - 48, Require
complete, in-range dot coverage before reporting diagnostics: in
scripts/diag/ppu2004_diff_vs_answerkey.py lines 42-48 and
scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py lines 42-48, validate
exactly dots 0 through 340, reject missing or out-of-range entries with a clear
error, and count only valid dots; in
scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py lines 20-23, require
complete coverage from both Mesen and RustyNES before generating either
comparison.

Source: Path instructions

scripts/diag/ppu2007_fetch_bus_dump.py (1)

39-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the reported dot range match the loop.

The banner promises dots 0..40, but range(0, 40) excludes dot 40. Either change the banner to 0..39 or iterate through range(0, 41).

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 43-43: Unnecessary start argument in range

Remove start argument

(PIE808)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_fetch_bus_dump.py` around lines 39 - 44, Align the
reconstructed fetch_bus output range by updating the banner and loop in the `fb`
reporting block so they describe and iterate over the same endpoints; preserve
the existing formatting and choose either dots 0..39 or inclusive dots 0..40.
scripts/diag/ppu2007_offset_greedy_search.py (1)

5-17: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate scan_dma_abort before parsing its output.

A failed subprocess or output without STRESS2007= makes [...][0] raise IndexError; a short payload then fails later during range(1,341,2). Check the return code, use a bounded timeout, locate the marker with a default, and validate the decoded length before scoring.

As per path instructions, one-off scripts should prioritize correctness and clear error messages over style.

Proposed fix
-    out=subprocess.run(['./target/release/scan_dma_abort',ROM,'30000'],capture_output=True,text=True,env=env).stdout
-    line=[l for l in out.splitlines() if 'STRESS2007=' in l][0].split('STRESS2007=')[1].strip()
+    proc = subprocess.run(
+        ['./target/release/scan_dma_abort', ROM, '30000'],
+        capture_output=True,
+        text=True,
+        env=env,
+        check=False,
+        timeout=60,
+    )
+    if proc.returncode != 0:
+        raise RuntimeError(f"scan_dma_abort failed: {proc.stderr.strip()}")
+    line = next(
+        (entry for entry in proc.stdout.splitlines() if "STRESS2007=" in entry),
+        None,
+    )
+    if line is None:
+        raise RuntimeError("scan_dma_abort did not emit STRESS2007")
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 6-6: Command coming from incoming request
Context: subprocess.run(['./target/release/scan_dma_abort',ROM,'30000'],capture_output=True,text=True,env=env)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.21)

[error] 6-6: Multiple statements on one line (semicolon)

(E702)


[error] 7-7: subprocess call: check for execution of untrusted input

(S603)


[warning] 7-7: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


[warning] 8-8: Prefer next(l for l in out.splitlines() if 'STRESS2007=' in l) over single element slice

Replace with next(l for l in out.splitlines() if 'STRESS2007=' in l)

(RUF015)


[error] 8-8: Ambiguous variable name: l

(E741)


[warning] 13-13: Consider [d[-1], *d[:-1]] instead of concatenation

Replace with [d[-1], *d[:-1]]

(RUF005)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_offset_greedy_search.py` around lines 5 - 17, Update run
to validate the scan_dma_abort subprocess before parsing: use a bounded timeout,
check the completed process return code, and raise a clear error that includes
relevant stderr on failure. Locate STRESS2007= safely without indexing an empty
match list, report missing or malformed output clearly, and verify the decoded
payload is long enough for the range(1,341,2) scoring before iterating.

Sources: Path instructions, Linters/SAST tools

scripts/diag/ppu2007_phase_offset_ranking.py (1)

25-28: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Handle out-of-range samples before hexadecimal formatting.

The first phase-5 entry uses d == 5; therefore val(d, -1) returns None (idx == -2). Formatting that value with :02X raises TypeError, so the script always crashes in this debug section.

Proposed fix
+def fmt(value):
+    return "--" if value is None else f"{value:02X}"
+
 for (j,d) in byphase[5][:12]:
-    print(f" j={j} dot={d} key={fullkey[j]:02X} o-1={val(d,-1):02X} o1={val(d,1):02X} o3={val(d,3):02X}")
+    print(
+        f" j={j} dot={d} key={fullkey[j]:02X}"
+        f" o-1={fmt(val(d, -1))} o1={fmt(val(d, 1))} o3={fmt(val(d, 3))}"
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

# Dump phase-5 expected vs our held for first 10 tiles
print("\nphase5 (pat-lo) j,readdot,key,our@-1,our@1,our@3:")

def fmt(value):
    return "--" if value is None else f"{value:02X}"

for (j,d) in byphase[5][:12]:
    print(
        f" j={j} dot={d} key={fullkey[j]:02X}"
        f" o-1={fmt(val(d, -1))} o1={fmt(val(d, 1))} o3={fmt(val(d, 3))}"
    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_phase_offset_ranking.py` around lines 25 - 28, Update
the phase-5 diagnostic loop around val so out-of-range samples such as val(d,
-1) returning None are handled before applying hexadecimal formatting. Preserve
the existing formatted output for valid values while rendering an explicit safe
placeholder for missing samples, preventing the debug section from raising
TypeError.
scripts/mapper-promotion/batch2.py (2)

62-63: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

zip_nes(src) can return None, crashing ines_mapper instead of recording an error.

When a staged .zip contains no .nes entry, zip_nes returns None, and ines_mapper(None) hits len(b)TypeError, aborting the whole batch. That defeats the per-item error collection the loop is built around (contrast the MISSING SRC path at Line 61).

Proposed guard
-    hm=ines_mapper(zip_nes(src))
+    nes=zip_nes(src)
+    if nes is None: errors.append((mid,"NO .nes IN ZIP",src)); continue
+    hm=ines_mapper(nes)
As per path instructions: "Prioritize correctness and clear error messages over style".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    nes=zip_nes(src)
    if nes is None: errors.append((mid,"NO .nes IN ZIP",src)); continue
    hm=ines_mapper(nes)
    if hm!=mid: errors.append((mid,f"HEADER {hm} != {mid}",src)); continue
🧰 Tools
🪛 Ruff (0.15.21)

[error] 63-63: Multiple statements on one line (colon)

(E701)


[error] 63-63: Multiple statements on one line (semicolon)

(E702)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/batch2.py` around lines 62 - 63, Guard the result of
zip_nes(src) before passing it to ines_mapper in the batch loop. When zip_nes
returns None, append a per-item error identifying the missing NES payload and
continue to the next item, matching the existing MISSING SRC handling; only call
ines_mapper for a valid payload.

Source: Path instructions


79-79: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Generated .rs output is written to a session-specific /tmp/claude-.../scratchpad/ path that won't exist on re-run. Both scripts hardcode a nested /tmp directory that is never created; open(..., "w") will raise FileNotFoundError on the missing parent after the script has already done all its scanning/copying work. Write next to the script (e.g. BASE/CWD) or a tempfile.mkstemp-derived path instead.

  • scripts/mapper-promotion/batch2.py#L79-L79: replace the hardcoded path with an output under a real, created directory (e.g. os.path.join(os.path.dirname(__file__), "promo_tests_2.rs")).
  • scripts/mapper-promotion/gen_promotion.py#L57-L57: apply the same fix for promo_tests.rs.

As per path instructions: "Prioritize correctness and clear error messages over style".

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 79-79: Use a context manager for opening files

(SIM115)


[error] 79-79: Probable insecure usage of temporary file or directory: "/tmp/claude-1000/-home-parobek-Code-OSS-Public-Projects-RustyNES/6112789e-19c9-4d7b-9638-b241cdbc833d/scratchpad/promo_tests_2.rs"

(S108)

📍 Affects 2 files
  • scripts/mapper-promotion/batch2.py#L79-L79 (this comment)
  • scripts/mapper-promotion/gen_promotion.py#L57-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/batch2.py` at line 79, The generated Rust outputs
use nonexistent hardcoded session-specific paths. Update the output writes in
scripts/mapper-promotion/batch2.py lines 79-79 and
scripts/mapper-promotion/gen_promotion.py lines 57-57 to target a real created
directory, such as each script’s directory via __file__, while preserving the
respective promo_tests_2.rs and promo_tests.rs filenames.

Sources: Path instructions, Linters/SAST tools

scripts/mapper-promotion/gen_promotion.py (1)

34-40: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

dirs[mid] (and roms[0]) raise opaque KeyError/IndexError when a promotion id lacks a staged dir/ROM.

enumerate_staged.py handles the same situation gracefully with dirs.get(mid) and a missing list; this script hard-indexes instead, so a single un-staged PROMOTE id aborts generation with a stack trace rather than a readable diagnostic.

Proposed guard
 for mid in PROMOTE:
-    d=dirs[mid]; full=os.path.join(EXT,d)
+    d=dirs.get(mid)
+    if d is None: print(f"SKIP m{mid}: no staged dir"); continue
+    full=os.path.join(EXT,d)
     roms=sorted([f for f in os.listdir(full) if f.lower().endswith((".nes",".zip"))])
+    if not roms: print(f"SKIP m{mid}: empty dir {d}"); continue
     chosen=None
As per path instructions: "Prioritize correctness and clear error messages over style".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    d=dirs.get(mid)
    if d is None: print(f"SKIP m{mid}: no staged dir"); continue
    full=os.path.join(EXT,d)
    roms=sorted([f for f in os.listdir(full) if f.lower().endswith((".nes",".zip"))])
    if not roms: print(f"SKIP m{mid}: empty dir {d}"); continue
    chosen=None
    for r in roms:
        b=rom_bytes(os.path.join(full,r))
        if b and ines_mapper(b)==mid: chosen=r; break
    if not chosen: chosen=roms[0]
🧰 Tools
🪛 Ruff (0.15.21)

[error] 34-34: Multiple statements on one line (semicolon)

(E702)


[error] 39-39: Multiple statements on one line (colon)

(E701)


[error] 39-39: Multiple statements on one line (semicolon)

(E702)


[error] 40-40: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/gen_promotion.py` around lines 34 - 40, Update the
promotion lookup around dirs[mid] and roms[0] to handle missing staged
directories and empty ROM lists without raising KeyError or IndexError. Use the
existing enumerate_staged.py pattern of safe directory lookup and missing-item
tracking, and emit a clear diagnostic for each unavailable promotion id while
allowing generation to continue for valid ids.

Source: Path instructions

scripts/pr-review/list_unresolved_threads.py (1)

5-7: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle empty comments and deleted authors without crashing.

comments.nodes may be empty, and GitHub can return a null author for deleted or unavailable users. The current indexing and c["author"]["login"] dereference abort the unresolved-thread listing. Use the same guarded extraction pattern as list_all_threads.py, with c.get("databaseId") and a fallback author value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_unresolved_threads.py` around lines 5 - 7, Update the
unresolved-thread output around the comments node extraction to handle empty
comments safely, using the guarded extraction pattern from list_all_threads.py
and skipping or safely handling threads without a first comment. Replace direct
databaseId and author dereferences with c.get("databaseId") and a fallback
author value when c.get("author") is null or unavailable.
scripts/pr-review/README.md (1)

15-22: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Paginate reviewThreads before claiming the audit is complete.

The query uses reviewThreads(first:100) without a cursor, so PRs with more than 100 threads are silently truncated. That can cause the closeout ceremony to miss unresolved threads despite the README describing list_all_threads.py as an audit of every thread. Add cursor pagination and merge all pages before piping the response to either filter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/README.md` around lines 15 - 22, Update the README’s
reviewThreads query to paginate through every page using the GraphQL cursor,
merging all thread nodes before piping the complete response to
list_unresolved_threads.py or list_all_threads.py. Preserve the existing thread
fields and ensure the audit does not stop after the first 100 results.
scripts/release-automation/split_commit.sh (1)

8-8: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Inspecting split_commit.sh and related release-automation context\n'

git ls-files scripts/release-automation/split_commit.sh crates/rustynes-mappers/src docs | sed -n '1,200p'

printf '\n--- split_commit.sh ---\n'
cat -n scripts/release-automation/split_commit.sh

printf '\n--- search for restore/staged/add patterns ---\n'
rg -n 'restore --staged|git add|split_commit|reset --soft|m119_tqrom|m004_mmc3|tqrom|mmc3' scripts/release-automation crates/rustynes-mappers docs -S

Repository: doublegate/RustyNES

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n scripts/release-automation/split_commit.sh | sed -n '1,80p'

Repository: doublegate/RustyNES

Length of output: 3065


Stage the deleted mapper paths too. git add only records the new m119_tqrom.rs and m004_mmc3.rs files here; after reset --soft and restore --staged ., the old tqrom.rs and mmc3.rs entries are still in the index, so this commit will keep both copies unless the deletions are staged as well.

Proposed fix
 $G add crates/rustynes-mappers/src/m119_tqrom.rs crates/rustynes-mappers/src/lib.rs crates/rustynes-mappers/src/m004_mmc3.rs crates/rustynes-test-harness/tests/v21_coverage_mappers.rs docs/mappers.md docs/compatibility.md
+$G add -u -- crates/rustynes-mappers/src/tqrom.rs crates/rustynes-mappers/src/mmc3.rs
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

$G add crates/rustynes-mappers/src/m119_tqrom.rs crates/rustynes-mappers/src/lib.rs crates/rustynes-mappers/src/m004_mmc3.rs crates/rustynes-test-harness/tests/v21_coverage_mappers.rs docs/mappers.md docs/compatibility.md
$G add -u -- crates/rustynes-mappers/src/tqrom.rs crates/rustynes-mappers/src/mmc3.rs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release-automation/split_commit.sh` at line 8, Update the git add
command in the release automation script to stage the deleted legacy tqrom.rs
and mmc3.rs paths alongside the new mapper files. Preserve staging of the
existing modified files so the reset/restore flow records both renames or
deletions correctly.
tests/roms/mmc5/README.md (1)

32-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the workspace crate names.

Use crates/rustynes-test-harness/... and rustynes-mappers; the retained nes-* paths are stale and make the integration-test reference unusable.

As per path instructions, “Docs are the spec here, not a changelog. Flag documentation that drifts from the code it describes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/roms/mmc5/README.md` around lines 32 - 35, Update the README reference
to use the current rustynes workspace names: replace the stale
crates/nes-test-harness path with crates/rustynes-test-harness and rename the
nes-mappers crate reference to rustynes-mappers, leaving the described test
behavior unchanged.

Source: Path instructions

to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md (1)

359-359: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the mapper-module path rename in the documentation.

Pre-rename mapper paths remain in two carryover notes.

  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L359-L359: replace the remaining sprintN.rs reference with the current board-numbered module layout.
  • to-dos/ROADMAP.md#L340-L340: replace mmc2_mmc4.rs with m010_mmc4.rs.
📍 Affects 2 files
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L359-L359 (this comment)
  • to-dos/ROADMAP.md#L340-L340
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md` at line 359, Update the
mapper-module references in both documentation sites: in
to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md at lines 359-359, replace the
remaining sprintN.rs reference with the current board-numbered module layout; in
to-dos/ROADMAP.md at lines 340-340, rename mmc2_mmc4.rs to m010_mmc4.rs.
crates/rustynes-mappers/src/jaleco_discrete.rs (1)

341-346: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Mapper-92 rationale comment contradicts the layout it documents. The comment states the JF-19 reset vector "lives in the fixed half," but with switchable_high == true the fixed bank serves $8000-$BFFF while $C000-$FFFF (containing the $FFFA-$FFFF vectors) is the switchable window — as m92_uses_5bit_prg_field confirms ($C000 → bank 17). The code is correct; the boot rationale is inverted and will mislead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs` around lines 341 - 346, The
documentation for switchable_high in the PRG window layout is inverted for
mapper 92: update the JF-19 rationale to state that the reset vectors at
$FFFA-$FFFF are in the switchable $C000-$FFFF window, consistent with
m92_uses_5bit_prg_field mapping $C000 to bank 17. Leave the layout and code
unchanged.
crates/rustynes-mappers/src/kaiser.rs (1)

1-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doc drift from the mapper module rename: banners and module docs no longer match the code. The rename relocated/renamed the mapper types but left the //! module headers and internal ==== board banners describing their pre-rename identities, so the board↔mapper-number reference is now wrong across three modules.

  • crates/rustynes-mappers/src/kaiser.rs#L1-L22: correct the module-doc board↔number pairs against the KaiserBoard enum (KS7017=303, KS7031=305, KS7016=306, KS7013B=312), add mapper 142/KS7032, and remove the orphaned "Waixing253 (mapper 253)" banner at L603-L610.
  • crates/rustynes-mappers/src/homebrew_boards.rs#L194-L200: retarget the misaligned banners at L194-L200 (→218), L515-L520 (→111), L671-L680 (→28), and drop/replace the orphaned "Mapper 63 — NTDEC" banner at L1184-L1202; add mappers 28 and 30 to the module doc.
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L52-L58: retarget the misaligned banners at L52-L58 (→86), L308-L322 (→72/92), L631-L641 (→101), drop the orphaned "Mapper 218" banner at L756-L766; add mappers 72/92/101 to the module doc.

As per coding guidelines: "Use comprehensive rustdoc and inline comments explaining rationale, architecture..."

📍 Affects 3 files
  • crates/rustynes-mappers/src/kaiser.rs#L1-L22 (this comment)
  • crates/rustynes-mappers/src/homebrew_boards.rs#L194-L200
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L52-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/kaiser.rs` around lines 1 - 22, Correct the stale
mapper documentation and banners without changing implementation behavior: in
crates/rustynes-mappers/src/kaiser.rs lines 1-22, align the module docs with
KaiserBoard, add mapper 142/KS7032, use KS7017=303, KS7031=305, KS7016=306, and
KS7013B=312, and remove the orphaned Waixing253 banner at lines 603-610; in
crates/rustynes-mappers/src/homebrew_boards.rs lines 194-200, retarget banners
at lines 194-200 to 218, 515-520 to 111, and 671-680 to 28, replace the orphaned
Mapper 63/NTDEC banner at lines 1184-1202, and add mappers 28 and 30 to the
module docs; in crates/rustynes-mappers/src/jaleco_discrete.rs lines 52-58,
retarget banners at lines 52-58 to 86, 308-322 to 72/92, and 631-641 to 101,
remove the orphaned Mapper 218 banner at lines 756-766, and add mappers 72, 92,
and 101 to the module docs.
crates/rustynes-mappers/src/m010_mmc4.rs (1)

242-287: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files crates/rustynes-mappers/src/m010_mmc4.rs crates/rustynes-mappers/src/m021_vrc4.rs crates/rustynes-mappers/src/m022_vrc2.rs crates/rustynes-mappers/src | sed -n '1,200p'

printf '\n--- m010_mmc4.rs ---\n'
ast-grep outline crates/rustynes-mappers/src/m010_mmc4.rs --view expanded || true
printf '\n--- m021_vrc4.rs ---\n'
ast-grep outline crates/rustynes-mappers/src/m021_vrc4.rs --view expanded || true
printf '\n--- m022_vrc2.rs ---\n'
ast-grep outline crates/rustynes-mappers/src/m022_vrc2.rs --view expanded || true

printf '\n--- search prg_ram / save_state / load_state ---\n'
rg -n "prg_ram|save_state\\(|load_state\\(" crates/rustynes-mappers/src/m010_mmc4.rs crates/rustynes-mappers/src/m021_vrc4.rs crates/rustynes-mappers/src/m022_vrc2.rs

Repository: doublegate/RustyNES

Length of output: 11991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  crates/rustynes-mappers/src/m010_mmc4.rs \
  crates/rustynes-mappers/src/m021_vrc4.rs \
  crates/rustynes-mappers/src/m022_vrc2.rs \
  crates/rustynes-mappers/src/m018_jaleco_ss88006.rs \
  crates/rustynes-mappers/src/m001_mmc1.rs
do
  printf '\n===== %s =====\n' "$f"
  wc -l "$f"
done

printf '\n===== m010_mmc4.rs relevant lines =====\n'
sed -n '1,320p' crates/rustynes-mappers/src/m010_mmc4.rs | cat -n | sed -n '1,290p'

printf '\n===== m021_vrc4.rs relevant lines =====\n'
sed -n '1,470p' crates/rustynes-mappers/src/m021_vrc4.rs | cat -n | sed -n '1,460p'

printf '\n===== m022_vrc2.rs relevant lines =====\n'
sed -n '1,360p' crates/rustynes-mappers/src/m022_vrc2.rs | cat -n | sed -n '1,350p'

printf '\n===== m018_jaleco_ss88006.rs save_state/load_state =====\n'
rg -n -A40 -B10 "fn save_state|fn load_state|prg_ram" crates/rustynes-mappers/src/m018_jaleco_ss88006.rs

printf '\n===== m001_mmc1.rs save_state/load_state =====\n'
rg -n -A35 -B10 "fn save_state|fn load_state|prg_ram" crates/rustynes-mappers/src/m001_mmc1.rs

Repository: doublegate/RustyNES

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n===== m010_mmc4.rs save/load =====\n'
sed -n '242,287p' crates/rustynes-mappers/src/m010_mmc4.rs | cat -n

printf '\n===== m021_vrc4.rs save/load =====\n'
sed -n '401,458p' crates/rustynes-mappers/src/m021_vrc4.rs | cat -n

printf '\n===== m022_vrc2.rs save/load =====\n'
sed -n '311,347p' crates/rustynes-mappers/src/m022_vrc2.rs | cat -n

printf '\n===== m018_jaleco_ss88006.rs save/load excerpts =====\n'
rg -n -A25 -B8 "fn save_state|fn load_state|SAVE_STATE_VERSION|prg_ram" crates/rustynes-mappers/src/m018_jaleco_ss88006.rs | head -n 120

printf '\n===== m001_mmc1.rs save/load excerpts =====\n'
rg -n -A25 -B8 "fn save_state|fn load_state|SAVE_STATE_VERSION|prg_ram" crates/rustynes-mappers/src/m001_mmc1.rs | head -n 120

Repository: doublegate/RustyNES

Length of output: 15284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n===== m018_jaleco_ss88006.rs save_state =====\n'
sed -n '470,560p' crates/rustynes-mappers/src/m018_jaleco_ss88006.rs | cat -n

printf '\n===== m018_jaleco_ss88006.rs load_state =====\n'
sed -n '560,640p' crates/rustynes-mappers/src/m018_jaleco_ss88006.rs | cat -n

printf '\n===== m001_mmc1.rs save_state =====\n'
sed -n '620,700p' crates/rustynes-mappers/src/m001_mmc1.rs | cat -n

printf '\n===== m001_mmc1.rs load_state =====\n'
sed -n '700,760p' crates/rustynes-mappers/src/m001_mmc1.rs | cat -n

Repository: doublegate/RustyNES

Length of output: 12227


Battery-backed PRG-RAM needs to be serialized with these save states.

  • crates/rustynes-mappers/src/m010_mmc4.rs: save_state/load_state cover only mapper registers + vram, so $6000-$7FFF contents are lost on round-trip.
  • crates/rustynes-mappers/src/m021_vrc4.rs: same omission; IRQ state round-trips, but prg_ram does not.
  • crates/rustynes-mappers/src/m022_vrc2.rs: same omission.

Append prg_ram to each blob, update the length check/cursor, and bump the state version byte.

📍 Affects 3 files
  • crates/rustynes-mappers/src/m010_mmc4.rs#L242-L287 (this comment)
  • crates/rustynes-mappers/src/m021_vrc4.rs#L401-L458
  • crates/rustynes-mappers/src/m022_vrc2.rs#L311-L347
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m010_mmc4.rs` around lines 242 - 287, Update
save_state/load_state in crates/rustynes-mappers/src/m010_mmc4.rs (lines
242-287), crates/rustynes-mappers/src/m021_vrc4.rs (lines 401-458), and
crates/rustynes-mappers/src/m022_vrc2.rs (lines 311-347) to bump the state
version, append each mapper’s prg_ram after the existing serialized data, and
include it in the expected-length calculation and load cursor before restoring
it. Ensure all existing registers, VRAM, and IRQ state continue to round-trip
unchanged.
crates/rustynes-mappers/src/m032_irem_g101.rs (1)

125-148: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fixed-bank bank_count - 2 underflows on an 8 KiB PRG image in both boards. Each read_prg derives bank_count from a ROM-supplied PRG size that new() permits to be a single 8 KiB bank, then subtracts 2 for the hardwired {-2} slot; 1usize - 2 panics under debug/overflow-checked builds (the trailing % bank_count only saves release). Sibling boards m076/m088 already guard this with saturating_sub.

  • crates/rustynes-mappers/src/m032_irem_g101.rs#L125-L148: replace both bank_count - 2 arms (mode-1 $8000 fixed and mode-0 $C000 fixed) with bank_count.saturating_sub(2).
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117: replace the slot-2 bank_count - 2 arm with bank_count.saturating_sub(2).

As per coding guidelines: "do not add ... panic!() ... applied to untrusted data parsed from ROMs ... Return a typed Result or error instead."

📍 Affects 2 files
  • crates/rustynes-mappers/src/m032_irem_g101.rs#L125-L148 (this comment)
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m032_irem_g101.rs` around lines 125 - 148,
Prevent fixed-bank index underflow for single-bank PRG images by replacing both
bank_count - 2 expressions in read_prg in
crates/rustynes-mappers/src/m032_irem_g101.rs:125-148 with saturating
subtraction, and make the same replacement for the slot-2 fixed-bank arm in
crates/rustynes-mappers/src/m033_taito_tc0190.rs:106-117; preserve the existing
modulo bank selection behavior without introducing panics.

Source: Coding guidelines

crates/rustynes-mappers/src/m036_txc_policeman.rs (3)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Glued .//! collapses two doc paragraphs and renders literal //!. Each line ends a sentence with .//! where a blank //! line (paragraph break) was intended; split into two lines so rustdoc renders the tier paragraph separately.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6: split ...accumulator chip.//! into ...accumulator chip. + a following //!.
  • crates/rustynes-mappers/src/m094_un1rom.rs#L5-L5: split ...no IRQ.//! likewise.
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7: split ...mirroring register.//! likewise.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L6-L6: split ...load-bearing.//! likewise.
📍 Affects 4 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6 (this comment)
  • crates/rustynes-mappers/src/m094_un1rom.rs#L5-L5
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L6-L6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` at line 6, Split the glued
Rust doc-comment lines so each sentence ends at its period and the following //!
marker is on its own blank documentation line, preserving separate rustdoc
paragraphs. Apply this in crates/rustynes-mappers/src/m036_txc_policeman.rs:6,
crates/rustynes-mappers/src/m094_un1rom.rs:5,
crates/rustynes-mappers/src/m095_namcot3425.rs:7, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:6.

53-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove stray mapper section banners describing the wrong mapper. The monolith→per-module split left banner comments describing adjacent mappers inside the wrong files; each precedes a struct or the test module and misdescribes the code it heads, which is misleading in a docs-as-spec crate.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65: delete the "Mapper 15 — K-1029" banner (file implements Txc36, mapper 36).
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L189-L196: delete the "Mapper 39 — Subor BNROM-like" banner.
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167: delete the "Mapper 79 — AVE NINA-03/06" banner (the L34-40 mapper-38 banner is correct; keep it).
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52: delete the "Mapper 15 — K-1029" banner.
  • crates/rustynes-mappers/src/m039_subor39.rs#L192-L203: delete the "Mapper 61" banner.
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43: delete the "Mapper 38 — Bit Corp" banner.
  • crates/rustynes-mappers/src/m041_caltron41.rs#L207-L216: delete the "Mapper 232 — Camerica Quattro" banner.
  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182: delete the "Mapper 101 — Jaleco JF-10" banner.
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71: delete the "Mapper 28 — Action 53" banner.
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L234-L249: delete the "Mapper 112 — NTDEC ASDER" banner.
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56: delete the "Mapper 15 — K-1029" banner.
  • crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244: delete the "Mapper 97 — Irem TAM-S1" banner.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65: delete the "Mapper 15 — K-1029" banner.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L233: delete the "Mapper 132 — TXC 22211" banner.
📍 Affects 8 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65 (this comment)
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L189-L196
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52
  • crates/rustynes-mappers/src/m039_subor39.rs#L192-L203
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43
  • crates/rustynes-mappers/src/m041_caltron41.rs#L207-L216
  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L234-L249
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56
  • crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L233
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` around lines 53 - 65,
Remove the stray mapper banner comment blocks at
crates/rustynes-mappers/src/m036_txc_policeman.rs:53-65 and 189-196;
crates/rustynes-mappers/src/m038_bitcorp38.rs:161-167;
crates/rustynes-mappers/src/m039_subor39.rs:40-52 and 192-203;
crates/rustynes-mappers/src/m041_caltron41.rs:37-43 and 207-216;
crates/rustynes-mappers/src/m094_un1rom.rs:177-182;
crates/rustynes-mappers/src/m095_namcot3425.rs:53-71 and 234-249;
crates/rustynes-mappers/src/m096_bandai96.rs:44-56 and 237-244; and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65 and 221-233. Keep the
correct mapper-38 banner in m038_bitcorp38.rs and leave all implementation code
unchanged.

198-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate #[cfg(test)] attribute on the test module. Harmless (the two are AND-ed) but a copy artifact; drop the second one at each site.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L198-L199: remove the duplicate #[cfg(test)].
  • crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185: remove the duplicate #[cfg(test)].
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252: remove the duplicate #[cfg(test)].
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L235-L236: remove the duplicate #[cfg(test)].
📍 Affects 4 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L198-L199 (this comment)
  • crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L235-L236
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` around lines 198 - 199,
Remove the redundant second #[cfg(test)] attribute at the test-module
declarations in crates/rustynes-mappers/src/m036_txc_policeman.rs#L198-L199,
crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185,
crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L235-L236, leaving one attribute
per module.
crates/rustynes-mappers/src/m069_sunsoft_fme7.rs (2)

515-534: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tail-length doc says 51 bytes; the actual TAIL_LEN is 47. The enumerated field list sums to 1 + 16 + 3*5 + 4 + 4 + 4 + 1 + 1 + 1 = 47, matching the TAIL_LEN constant and write_tail/read_tail. Only the -- 51 bytes total -- summary (line 534) and the "TAIL_LEN (51 bytes)" note (line 926) are wrong. The constant is authoritative, so this is doc-only, but it misdescribes a save-state binary format.

Proposed doc corrections
-    ///   envelope.holding   : 1 (bool)
-    ///   -- 51 bytes total --
+    ///   envelope.holding   : 1 (bool)
+    ///   -- 47 bytes total --
-        // Tail size = Sunsoft5BAudio::TAIL_LEN (51 bytes).
+        // Tail size = Sunsoft5BAudio::TAIL_LEN (47 bytes).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m069_sunsoft_fme7.rs` around lines 515 - 534,
Correct the save-state documentation for the tail length: change the “51 bytes
total” summary near the tail layout and the “TAIL_LEN (51 bytes)” note near the
TAIL_LEN reference to state 47 bytes. Do not modify TAIL_LEN, write_tail, or
read_tail, since their 47-byte format is authoritative.

922-946: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

CHR-RAM is mutated but never persisted in two new mappers' save states. Both boards allocate CHR-RAM when the header carries no CHR-ROM and mutate it via ppu_write, yet their save_state/load_state serialize only register scalars, prg_ram, and vram. Because the load-side expected length also omits CHR, the round-trip silently discards CHR-RAM instead of erroring. Sibling mappers m068/m070/m071 already serialize chr when chr_is_ram.

  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs#L922-L946: serialize chr_rom when chr_is_ram; because this blob is versioned (v1/v2 + audio tail), gate the new field behind a v3 bump (or a conditional tail) so existing v1/v2 states still load, and extend load_state's core_expected accordingly.
  • crates/rustynes-mappers/src/m075_vrc1.rs#L179-L219: append chr_rom after vram when chr_is_ram in save_state, and add the matching read plus chr_ram_len term to expected in load_state (see per-site diff).
📍 Affects 2 files
  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs#L922-L946 (this comment)
  • crates/rustynes-mappers/src/m075_vrc1.rs#L179-L219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m069_sunsoft_fme7.rs` around lines 922 - 946,
Persist CHR-RAM in both mapper save-state implementations: in
crates/rustynes-mappers/src/m069_sunsoft_fme7.rs lines 922-946, update
save_state/load_state to serialize and restore chr when chr_is_ram, version the
existing v1/v2 format safely (for example via v3 or a conditional tail), and
include the CHR length in load_state’s core_expected while preserving
older-state compatibility; in crates/rustynes-mappers/src/m075_vrc1.rs lines
179-219, append chr after vram in save_state when chr_is_ram, read it during
load_state, and add chr_ram_len to expected.
crates/rustynes-mappers/src/m076_namcot3446.rs (1)

55-73: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale template comments describe the wrong mappers. The block at lines 55-73 documents "Mapper 28 — Action 53" and the block at lines 217-225 documents "Mapper 174 — NTDEC 5-in-1"; neither belongs in the Namcot 3446 (mapper 76) module and both will mislead a reader about what this file implements. Also: line 8 lost a newline (...m095_namcot3425.rs.//!renders the//!as literal text), and#[cfg(test)]` is duplicated at lines 227-228. These are copy-paste residue from the module split; delete the foreign blocks, fix the line-8 break, and drop the duplicate attribute.

Also applies to: 217-228

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs` around lines 55 - 73, Remove
the foreign Action 53 and NTDEC 5-in-1 documentation blocks from the Namcot 3446
mapper module, preserving only comments relevant to mapper 76. Restore the
missing newline before the module’s `//!` documentation at the top, and remove
the duplicated adjacent `#[cfg(test)]` attribute while retaining a single test
configuration attribute.
crates/rustynes-mappers/src/m085_vrc7.rs (1)

535-632: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the core/bus snapshot path persist mapper SRAM/WRAM via sram()/sram_mut(),
# independently of Mapper::save_state? If not, VRC7 prg_ram is lost across snapshots.
rg -nP -C4 '\b(sram|sram_mut)\s*\(' crates/rustynes-core/src | rg -n -C4 'snapshot|save_state|restore|serialize|deserialize|battery' 
echo '--- mapper snapshot call sites ---'
rg -nP -C3 '\.(save_state|load_state)\s*\(' crates/rustynes-core/src
echo '--- schema audit expectations for mapper WRAM ---'
fd -t f 'snapshot_schema_audit.rs' crates | xargs -r rg -nP -C3 'prg_ram|sram|WRAM|exclude'

Repository: doublegate/RustyNES

Length of output: 2860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bus snapshot writer around MAP section ---'
sed -n '2680,2825p' crates/rustynes-core/src/bus.rs

echo '--- search for SRAM/WRAM snapshot sections in core ---'
rg -nP -C3 '\b(SRAM|WRAM|sram|battery|save_ram|ram)\b' crates/rustynes-core/src/bus.rs

echo '--- search for snapshot tags definitions ---'
rg -nP -C3 'tag::(MAP|.*RAM|.*SRAM|.*WRAM)|pub mod tag' crates/rustynes-core/src

Repository: doublegate/RustyNES

Length of output: 16777


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- VRC7 mapper: prg_ram field, SRAM accessors, save/load state ---'
rg -nP -C4 '\bprg_ram\b|fn (save_state|load_state|sram|sram_mut)\b|Lagrange Point|self-test' crates/rustynes-mappers/src/m085_vrc7.rs

echo '--- VRC7 save-state round-trip test coverage ---'
rg -nP -C4 'vrc7_save_state_round_trip|save_state_round_trip|prg_ram|sram' crates/rustynes-mappers/src crates/rustynes-mappers/tests

echo '--- save-state audit entries mentioning VRC7 or prg_ram ---'
rg -nP -C4 'VRC7|m085|prg_ram|WRAM|SRAM' crates/**/tests crates/**/src | head -n 200

Repository: doublegate/RustyNES

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- VRC7 file map: prg_ram, sram, save/load state ---'
rg -n -C3 'prg_ram|fn sram|fn sram_mut|fn save_state|fn load_state|save_state_round_trip' crates/rustynes-mappers/src/m085_vrc7.rs

echo '--- VRC7 struct/impl shape ---'
sed -n '1,220p' crates/rustynes-mappers/src/m085_vrc7.rs

echo '--- VRC7 save/load state region ---'
sed -n '500,680p' crates/rustynes-mappers/src/m085_vrc7.rs

Repository: doublegate/RustyNES

Length of output: 20750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l crates/rustynes-mappers/src/m085_vrc7.rs

Repository: doublegate/RustyNES

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l crates/rustynes-mappers/src/m085_vrc7.rs
echo '--- VRC7 file map: prg_ram, sram, save/load state ---'
rg -n -C3 'prg_ram|fn sram|fn sram_mut|fn save_state|fn load_state|save_state_round_trip' crates/rustynes-mappers/src/m085_vrc7.rs
echo '--- VRC7 save/load state region ---'
sed -n '500,680p' crates/rustynes-mappers/src/m085_vrc7.rs

Repository: doublegate/RustyNES

Length of output: 12624


Persist VRC7 WRAM in the snapshot blob
save_state/load_state omit prg_ram, and the core snapshot path only stores Mapper::save_state() for MAP. That drops the 8 KiB $6000-$7FFF RAM across save/restore and rewind; append it here, bump the version, and add a round-trip assertion for a $6000 write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m085_vrc7.rs` around lines 535 - 632, Update M085
save_state and load_state to persist the 8 KiB prg_ram after the existing VRAM
data, bump the state version, and adjust size validation and offsets
accordingly. Preserve compatibility expectations for the new version while
rejecting unsupported versions. Add a round-trip test covering a write to $6000
and verifying the value after save/load.
crates/rustynes-mappers/src/m087_jaleco87.rs (3)

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Justify or narrow the lint allowances.

The module-level and test-module #[allow] attributes suppress Clippy lints without explaining the invariants or why the suppression is necessary. Add concise rationale comments, or remove the allowances by avoiding the casts/suppressed documentation patterns.

As per coding guidelines, local #[allow] attributes must be justified.

Also applies to: 180-180

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs` at line 19, The module-level
and test-module Clippy allowances in m087_jaleco87 must be justified or
narrowed. Add concise comments explaining the invariant and necessity for each
allowed lint, or remove the allowances by eliminating the triggering casts and
documentation patterns; apply the same treatment to both visible allow
attributes.

Source: Coding guidelines


61-65: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject CHR-ROMs larger than the addressable bank space.

The register exposes only two CHR-bank bits, so this implementation can select at most four 8 KiB banks. Larger inputs are accepted, but chr_bank can never reach the additional banks; the modulo in ppu_read therefore silently renders only the first four banks. Reject sizes above 4 * CHR_BANK_8K or implement the missing bank-selection bits. Mapper 87’s documented capacity also tops out at 32 KiB. (nesdev.org)

Proposed validation
-        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
+        if chr_rom.is_empty()
+            || !chr_rom.len().is_multiple_of(CHR_BANK_8K)
+            || chr_rom.len() > 4 * CHR_BANK_8K
+        {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if chr_rom.is_empty()
            || !chr_rom.len().is_multiple_of(CHR_BANK_8K)
            || chr_rom.len() > 4 * CHR_BANK_8K
        {
            return Err(MapperError::Invalid(format!(
                "Mapper-87 expects non-empty CHR-ROM in 8 KiB units, got {} bytes",
                chr_rom.len()
            )));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs` around lines 61 - 65, Update
the CHR-ROM validation in the mapper initialization path to reject sizes greater
than 4 * CHR_BANK_8K, alongside the existing empty and 8 KiB alignment checks.
Preserve the current Invalid error behavior and include the oversized length in
the validation message; do not alter bank selection logic.

235-246: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover VRAM restoration in the save-state test.

The test only verifies chr_bank; it would still pass if self.vram were omitted from the state or restored incorrectly. Write a distinctive nametable byte before saving and assert it after loading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs` around lines 235 - 246, Update
the save_state_round_trip test for Jaleco87 to write a distinctive nametable
byte through the first mapper instance before save_state, then assert the same
byte at the corresponding PPU address after load_state into the second instance.
Keep the existing CHR-bank assertion and use the existing cpu_write/ppu_read
APIs.
crates/rustynes-mappers/src/m095_namcot3425.rs (1)

149-158: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

last - 1 underflows for a single-bank (8 KiB) PRG image.

last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1, so last == 0 for a 1-bank ROM; the $C000-$DFFF arm then computes last - 1, a usize underflow. Under overflow-checks (debug/test) this panics on a CPU read — exactly the no_std/no-panic-on-untrusted-ROM invariant this crate documents. new() accepts any non-zero multiple of 8 KiB, so a crafted 8 KiB mapper-95 image reaches it. Use a saturating subtraction.

As per coding guidelines, "do not add .unwrap()/.expect()/panic!() when applied to untrusted data parsed from ROMs".

Proposed fix
-            0xC000..=0xDFFF => self.read_prg(last - 1, addr),
+            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn cpu_read(&mut self, addr: u16) -> u8 {
        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        match addr {
            0x8000..=0x9FFF => self.read_prg(self.prg_banks[0] as usize, addr),
            0xA000..=0xBFFF => self.read_prg(self.prg_banks[1] as usize, addr),
            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),
            0xE000..=0xFFFF => self.read_prg(last, addr),
            _ => 0,
        }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs` around lines 149 - 158,
Update the cpu_read method’s $C000-$DFFF mapping to use saturating subtraction
when deriving the preceding PRG bank from last. Preserve the existing bank
selection for multi-bank ROMs while ensuring single-bank images resolve safely
without underflow or panic.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 44

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/release-notes/v2.2.3.md:
- Line 19: Update the release note’s serde migration description to state that
the [emulation] configuration uses #[serde(default = "default_fast_dotloop")],
matching the implementation in config.rs and documenting that older configs
default fast_dotloop to enabled.
- Around line 73-77: The release note’s movie/netplay compatibility statement is
inaccurate. Replace the claim that both re-derive state from a fresh power-on
with the precise scope: old .rns files are rejected, .rnm playback remains
accepted, and in-memory netplay rollback and TAS-seeking snapshots use the
updated v8 schema.

In @.github/workflows/pgo.yml:
- Around line 58-74: Add an explicit read-only permissions block with contents:
read to the reusable workflow or the pgo job in .github/workflows/pgo.yml.
Ensure build, benchmark, and test steps retain required repository access, while
any write permissions remain limited to the publish job.

In `@AGENTS.md`:
- Line 188: Update the release-history wording in AGENTS.md so the v2.2.2
“Conduit” reference says “the prior release” instead of “this release.” Preserve
the surrounding version chronology and all other release descriptions unchanged.

In `@CHANGELOG.md`:
- Around line 37-40: Reconcile the test totals reported in the CHANGELOG entry
with the corresponding count in the v2.2.3 release notes. Update the affected
verification summary to use one consistent count, or explicitly label the
differing command/subset and explain the distinction so both release artifacts
accurately describe their results.

In `@crates/rustynes-core/src/bus.rs`:
- Around line 1631-1654: Remove the copied expansion-device and port panic
documentation immediately before set_zapper_temporal_light, leaving only
documentation describing the temporal Zapper light model, its default-off
behavior, and rationale. Keep the correct expansion-device contract with
set_expansion_device where it already appears.
- Around line 2503-2507: Update the zapper temporal-light branch in the bus read
path to handle the signed result of Ppu::scanline() explicitly: return the
temporal no-light byte for negative scanlines, and only convert nonnegative
scanlines to u16 before calling Zapper::read_at_scanline. Remove the
unwrap_or(0) fallback so the pre-render line is never treated as visible
scanline zero.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs`:
- Around line 52-58: Remove the four orphaned mapper banner comment blocks
before the unrelated Jaleco structs, or replace them with accurate headers for
the following Mapper 86, 72, 101, and test-related code. Ensure no comments
incorrectly describe Mapper 38, 41, 77, or 218 in this file.

In `@crates/rustynes-mappers/src/m009_mmc2.rs`:
- Around line 277-278: Remove one redundant #[cfg(test)] attribute at the tests
module in crates/rustynes-mappers/src/m009_mmc2.rs lines 277-278,
crates/rustynes-mappers/src/m021_vrc4.rs lines 461-462, and
crates/rustynes-mappers/src/m022_vrc2.rs lines 350-351, leaving one attribute at
each site.

In `@crates/rustynes-mappers/src/m032_irem_g101.rs`:
- Around line 125-148: Update the fixed-bank calculations in read_prg,
specifically both bank_count - 2 expressions for the $8000/$C000 slots, to use
saturating subtraction so single-bank PRG-ROMs cannot underflow. Preserve the
existing modulo bank_count mapping and all other slot behavior.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs`:
- Around line 106-117: Update read_prg so fixed-tail bank selection for slots 2
and 3 cannot underflow when bank_count is less than two. Preserve the normal
second-last/last mapping for larger images while ensuring single-bank PRG reads
remain in range and never panic during register access.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs`:
- Around line 53-71: Remove the orphaned mapper comment blocks and redundant
duplicate cfg(test) attributes across the listed mapper files, retaining only
each file’s own mapper documentation. In m036_txc_policeman.rs remove the Mapper
15 and Mapper 39 blocks plus the duplicate cfg(test); in m038_bitcorp38.rs
remove Mapper 79; in m039_subor39.rs remove Mapper 15 and Mapper 61; in
m095_namcot3425.rs resolve the missing-implementation verification first, then
remove Mapper 28, Mapper 112, and the duplicate cfg(test); in m096_bandai96.rs
remove Mapper 15 and Mapper 97; and in m097_irem_tam_s1.rs remove Mapper 15,
Mapper 132, and the duplicate cfg(test).
- Line 6: Restore the crate-doc paragraph breaks by splitting the collapsed
trailing doc markers: in crates/rustynes-mappers/src/m036_txc_policeman.rs lines
6-6, crates/rustynes-mappers/src/m095_namcot3425.rs lines 7-7, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs lines 6-6, keep each
sentence-ending period on its own `//!` line and add the following blank `//!`
line.

In `@crates/rustynes-mappers/src/m041_caltron41.rs`:
- Around line 37-43: Remove the stale board comment blocks, duplicate test
attributes, and joined documentation lines from the split mapper modules. In
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43, remove the Mapper 38
block and also remove the Mapper 232 block at lines 207-216; in
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs lines 80-96, remove the
Mmc3Clone and Mapper 50 blocks, reduce the duplicated #[cfg(test)] at lines
312-313 to one, and put the joined //! text at line 12 on its own line; apply
the corresponding removals, attribute collapse, and //! line split in
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs lines 54-70 and
crates/rustynes-mappers/src/m076_namcot3446.rs lines 55-73 as specified.

In `@crates/rustynes-mappers/src/m048_taito_tc0690.rs`:
- Around line 132-143: Update read_prg’s fixed-bank mapping for slot 2 to use
saturating subtraction when deriving bank_count - 2, matching the
m076_namcot3446 convention. Preserve the existing slot mapping and modulo
behavior while ensuring single-bank PRG reads cannot underflow.

In `@crates/rustynes-mappers/src/m077_irem_napoleon.rs`:
- Around line 222-223: Remove the second consecutive #[cfg(test)] attribute
above mod tests in crates/rustynes-mappers/src/m077_irem_napoleon.rs:222-223,
crates/rustynes-mappers/src/m079_ave_nina03_06.rs:226-227, and
crates/rustynes-mappers/src/m094_un1rom.rs:184-185, leaving one attribute per
tests module.

In `@crates/rustynes-mappers/src/m085_vrc7.rs`:
- Around line 535-632: Update the VRC7 snapshot methods save_state and
load_state to preserve and restore the live self.opll synthesizer state together
with its clock/sample counters, rather than serializing only self.audio.regs.
Bump the snapshot version and append the OPLL state to the format, or explicitly
rebuild self.opll from the restored serialized fields, ensuring restored audio
remains synchronized after loading.

In `@crates/rustynes-mappers/src/m093_sunsoft3r.rs`:
- Around line 106-112: Update M093’s cpu_write method to apply the bus-conflict
mask by ANDing the incoming value with self.cpu_read(addr) before decoding the
register. Use the masked value for both prg_bank and chr_ram_enabled extraction,
while preserving the existing 0x8000..=0xFFFF address guard.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs`:
- Around line 40-47: Remove the stale unrelated section-divider comments at
crates/rustynes-mappers/src/m107_magic_dragon107.rs:40-47 and :168-175;
crates/rustynes-mappers/src/m113_ave_nina006.rs:48-54 and :208-217;
crates/rustynes-mappers/src/m132_txc_22211.rs:58-70 and :268-275;
crates/rustynes-mappers/src/m136_sachen_3011.rs:306-315;
crates/rustynes-mappers/src/m156_daou156.rs:42-58 and :229-244;
crates/rustynes-mappers/src/m177_hengedianzi.rs:52-59 and :182-188;
crates/rustynes-mappers/src/m179_hengedianzi.rs:53-60 and :183-193;
crates/rustynes-mappers/src/m180_nichibutsu180.rs:42-58 and :215-229; and
crates/rustynes-mappers/src/m185_cnrom185.rs:46-62 and :234-245. Delete each
misleading mapper or board divider without changing the mapper implementations
or replacing it with unrelated documentation.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs`:
- Around line 219-220: Remove the duplicate #[cfg(test)] attribute above the
tests module, retaining exactly one in
crates/rustynes-mappers/src/m113_ave_nina006.rs lines 219-220,
crates/rustynes-mappers/src/m132_txc_22211.rs lines 277-278,
crates/rustynes-mappers/src/m177_hengedianzi.rs lines 190-191, and
crates/rustynes-mappers/src/m179_hengedianzi.rs lines 195-196.
- Line 6: Fix the collapsed rustdoc paragraph breaks by separating the prose
from the literal `//!` marker in
crates/rustynes-mappers/src/m113_ave_nina006.rs:6-6,
crates/rustynes-mappers/src/m136_sachen_3011.rs:11-11,
crates/rustynes-mappers/src/m177_hengedianzi.rs:5-5, and
crates/rustynes-mappers/src/m179_hengedianzi.rs:6-6; end each preceding sentence
on its own line and add an empty `//!` line before the “best-effort (Tier-2)”
paragraph.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs`:
- Around line 206-209: Update the PRG read branch for 0x8000..=0xFFFF to wrap
the computed offset against self.prg_rom.len(), not count * PRG_BANK_32K, so
every valid non-empty ROM length accepted by check_prg remains in bounds.
Preserve the existing bank-address calculation and avoid introducing unwrap,
expect, or panic handling for ROM-derived data.

In `@crates/rustynes-mappers/src/m232_camerica_bf9096.rs`:
- Around line 38-44: Remove the stale orphan mapper section-divider comment
blocks, without changing surrounding implementation: delete the specified blocks
in crates/rustynes-mappers/src/m232_camerica_bf9096.rs at 38-44 and 191-200;
crates/rustynes-mappers/src/m240_cne_multicart.rs at 47-53 and 177-183;
crates/rustynes-mappers/src/m241_bxrom241.rs at 35-41;
crates/rustynes-mappers/src/m244_cne_decathlon.rs at 47-53 and 195-207;
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs at 42-59; and
crates/rustynes-mappers/src/m250_nitra250.rs at 42-58.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs`:
- Line 5: Fix the module-header formatting in
crates/rustynes-mappers/src/m240_cne_multicart.rs:5-5 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs:5-5 by placing the trailing
//! on its own blank documentation line. Remove the duplicate #[cfg(test)]
attribute in crates/rustynes-mappers/src/m240_cne_multicart.rs:185-186 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs:209-210, leaving one attribute
for each test module.

In `@crates/rustynes-mappers/src/m246_fong_shen_bang246.rs`:
- Around line 8-15: Update the module docstrings in
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs lines 8-15 and
crates/rustynes-mappers/src/m250_nitra250.rs lines 9-16 to classify both mappers
as Curated (Tier-1), replacing the BestEffort/Tier-2 wording and
MapperTier::BestEffort reference while leaving implementation behavior
unchanged.

In `@crates/rustynes-mappers/src/m250_nitra250.rs`:
- Around line 127-137: Update prg_bank_for so the fixed-bank expression for the
(false, 0xC000..=0xDFFF) and (true, 0x8000..=0x9FFF) arms cannot underflow when
last is zero. Use saturating subtraction, matching the safe handling in
m004_mmc3, while preserving the existing bank selection for larger PRG images.

In `@crates/rustynes-mappers/src/ntdec.rs`:
- Around line 60-78: Replace or remove the stale mapper header blocks so each
comment documents the implementation or test module that follows, without
changing banking, IRQ, or save-state logic. In
crates/rustynes-mappers/src/ntdec.rs at lines 60-78, 246, 411, 727, 901, 1123,
and 1327, update headers for Ntdec63, Ntdec174, mapper-40, NtdecAsder112,
NtdecTc112 (193), NtdecN625092 (221), and the test module respectively. Apply
the corresponding corrections in crates/rustynes-mappers/src/sachen_8259.rs at
lines 58-74, 248, and 482; crates/rustynes-mappers/src/sachen_discrete.rs at
lines 62-74, 1273, and 1415; crates/rustynes-mappers/src/waixing.rs at lines
63-81, 270, and 647; remove or rewrite the nonexistent TxcChip header in
crates/rustynes-mappers/src/m513_sachen_9602.rs at lines 341-344; and correct
the misplaced Sachen 8259 header in crates/rustynes-mappers/src/mmc3_clones.rs
at lines 767-780 before its test module.

In `@crates/rustynes-ppu/src/snapshot.rs`:
- Line 491: Update the snapshot version validation around the version check to
use the `PPU_SNAPSHOT_VERSION` constant as the upper bound instead of the
duplicated literal `8`, preserving acceptance of versions from 1 through the
current schema version.

In `@docs/performance.md`:
- Around line 1049-1050: Update the release-tag command immediately following
the documented release.yml flow to replace the obsolete v1.2.0 example with the
current v2.2.3 tag or a clearly future placeholder, keeping the surrounding
release instructions unchanged.
- Around line 692-696: Correct the remaining-dot breakdown in the performance
documentation to explicitly include visible-line dot 0 alongside dots 257..=340
for 20,400 dots, then list post-render line 240 as 341, vblank lines 241–260 as
6,820, and pre-render line 261 as 341, preserving the 27,902 total.

In `@docs/ppu-2c02.md`:
- Around line 242-265: The save-state coverage section must document v8 as a
compatibility epoch: state files from v2.2.2 and earlier must be rejected with a
version error rather than upconverted. Add this caller-visible behavior near the
PPU_SNAPSHOT_VERSION v8 discussion, or link directly to the authoritative
compatibility policy required by ADR 0028.

In `@scripts/diag/locate_first_trace_divergence.py`:
- Around line 9-28: Validate that the diagnostic receives both required path
arguments before `load(sys.argv[1])` accesses `sys.argv`. When either argument
is missing, terminate with a clear usage message describing the expected
invocation; otherwise preserve the existing row-processing and output behavior.

In `@scripts/diag/ppu2002_read_value_histogram.py`:
- Line 3: Update the file handling around the histogram input and output paths
to avoid the predictable /tmp/RustyNES location and fixed filenames. Accept
explicit paths or create a securely owned temporary directory/files using
exclusive creation, and ensure both the open at the top level and the output
handling near the script’s line 37 use the safe path strategy.
- Around line 30-36: Update the diagnostic window logic around the loop and its
out.append call to require all four rows share the prerender scanline identified
by the surrounding timing checks and have consecutive dots following the
expected progression. Only append a histogram match after validating these
timing constraints in addition to the existing mask sequence checks.

In `@scripts/diag/ppu2004_diff_vs_answerkey.py`:
- Around line 42-48: Validate that all 341 expected dot indices are present
before reporting comparison results: update
scripts/diag/ppu2004_diff_vs_answerkey.py lines 42-48 and
scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py lines 42-48 to treat
missing indices as capture errors or mismatches, and update
scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py lines 20-23 to validate
both dot maps before comparison.

In `@scripts/diag/ppu2007_offset_greedy_search.py`:
- Around line 11-16: Update the rotation candidates in the greedy score
calculation to include both permitted one-dot rotations: retain the unrotated
dump and existing right rotation, and add the opposite left rotation using d[1:]
+ d[:1]. Ensure best scoring evaluates all three candidates.
- Around line 7-9: Update the diagnostic command handling around subprocess.run
and the STRESS2007 parsing to require a successful scan_dma_abort exit, preserve
stderr for failure reporting, and raise a clear error when the command fails,
the marker is absent, or its value is not valid hexadecimal. Replace the
unchecked list indexing in the parsing flow while preserving the existing data
conversion for valid output.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py`:
- Line 22: Validate the command-line arguments before the file read in the
script’s top-level flow, ensuring a missing dump-file argument prints clear
usage information and exits with a command-line error instead of allowing
sys.argv[1] to raise IndexError. Keep the existing open/read behavior unchanged
when the required argument is present.

In `@scripts/mapper-promotion/batch2.py`:
- Line 79: Replace the hard-coded session-specific output path used by
batch2.py’s generator with a configurable output-path argument or deliberate
repository-local default, and apply the same output-path contract to
gen_promotion.py. Ensure both generators write generated test bodies to an
existing, valid destination while preserving their current output content.

In `@scripts/mapper-promotion/enumerate_staged.py`:
- Around line 60-65: Prevent failed mapper-header matches from being promoted:
in scripts/mapper-promotion/enumerate_staged.py lines 60-65, keep the mismatch
in failure reporting but omit it from rows and promoted_ids instead of selecting
roms[0]; in scripts/mapper-promotion/gen_promotion.py lines 36-43, fail
generation for an unmatched mapper rather than emitting a fallback ROM path or
extended_m{mid} output.

In `@scripts/mapper-promotion/scan.py`:
- Around line 20-28: Update get_header so ZIP archives process every .nes member
instead of selecting names[0], allowing mapper classification to inspect all
ROMs and avoid archive-order dependence. Preserve each member’s archive path
when constructing the reported candidate, or explicitly reject archives
containing multiple .nes members with a clear error; keep the existing
single-file behavior unchanged.

In `@scripts/pr-review/list_unresolved_threads.py`:
- Around line 6-7: Update the output statements in the unresolved-thread listing
to sanitize every pull-request-sourced field before printing, including the
thread path, author login, and truncated body. Apply a visible escaping or
encoding step that covers ESC and C1 control characters while preserving the
existing newline flattening and output structure.

In `@tests/roms/assorted/README.md`:
- Around line 34-35: Update the cpu_timing_test.nes entry in the README to
describe its end-to-end per-instruction CPU cycle-counting behavior without
claiming it is more comprehensive than cpu_timing_test6. Keep the description
consistent with the fixture’s documented source path in LICENSES.md.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`:
- Around line 358-360: Complete the mapper-module path migration in both
documentation references: in to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md lines
358-360, update the remaining crates/rustynes-mappers/src/sprintN.rs reference
or explicitly mark it historical; in to-dos/ROADMAP.md lines 334-340, replace
crates/rustynes-mappers/src/mmc2_mmc4.rs with
crates/rustynes-mappers/src/m010_mmc4.rs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d73229f6-df55-4d3e-9057-a70ff0d48fe9

📥 Commits

Reviewing files that changed from the base of the PR and between af47fe6 and f8974f0.

⛔ Files ignored due to path filters (33)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snap is excluded by !**/*.snap
📒 Files selected for processing (214)
  • .github/actionlint.yaml
  • .github/release-notes/v2.2.3.md
  • .github/workflows/antigravity-review.yml
  • .github/workflows/ci.yml
  • .github/workflows/ios.yml
  • .github/workflows/pgo.yml
  • .github/workflows/release.yml
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/rustynes-apu/src/snapshot.rs
  • crates/rustynes-core/Cargo.toml
  • crates/rustynes-core/benches/full_frame.rs
  • crates/rustynes-core/benches/snapshot_restore.rs
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
  • crates/rustynes-core/src/nes.rs
  • crates/rustynes-frontend/src/app.rs
  • crates/rustynes-frontend/src/config.rs
  • crates/rustynes-frontend/src/debugger/settings_panel.rs
  • crates/rustynes-frontend/src/history_viewer.rs
  • crates/rustynes-frontend/src/runahead.rs
  • crates/rustynes-mappers/src/bmc_simple.rs
  • crates/rustynes-mappers/src/fds.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/kaiser.rs
  • crates/rustynes-mappers/src/lib.rs
  • crates/rustynes-mappers/src/m000_nrom.rs
  • crates/rustynes-mappers/src/m001_mmc1.rs
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m003_cnrom.rs
  • crates/rustynes-mappers/src/m004_mmc3.rs
  • crates/rustynes-mappers/src/m005_mmc5.rs
  • crates/rustynes-mappers/src/m007_axrom.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m010_mmc4.rs
  • crates/rustynes-mappers/src/m011_color_dreams.rs
  • crates/rustynes-mappers/src/m013_cprom.rs
  • crates/rustynes-mappers/src/m016_bandai_fcg.rs
  • crates/rustynes-mappers/src/m018_jaleco_ss88006.rs
  • crates/rustynes-mappers/src/m019_namco163.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m024_vrc6.rs
  • crates/rustynes-mappers/src/m032_irem_g101.rs
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs
  • crates/rustynes-mappers/src/m034_bnrom_nina001.rs
  • crates/rustynes-mappers/src/m035_jy_asic.rs
  • crates/rustynes-mappers/src/m036_txc_policeman.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs
  • crates/rustynes-mappers/src/m064_rambo1.rs
  • crates/rustynes-mappers/src/m065_irem_h3001.rs
  • crates/rustynes-mappers/src/m066_gxrom.rs
  • crates/rustynes-mappers/src/m067_sunsoft3.rs
  • crates/rustynes-mappers/src/m068_sunsoft4.rs
  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs
  • crates/rustynes-mappers/src/m070_bandai74.rs
  • crates/rustynes-mappers/src/m071_camerica_bf9093.rs
  • crates/rustynes-mappers/src/m073_vrc3.rs
  • crates/rustynes-mappers/src/m075_vrc1.rs
  • crates/rustynes-mappers/src/m076_namcot3446.rs
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs
  • crates/rustynes-mappers/src/m078_irem_jaleco78.rs
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs
  • crates/rustynes-mappers/src/m080_taito_x1_005.rs
  • crates/rustynes-mappers/src/m082_taito_x1_017.rs
  • crates/rustynes-mappers/src/m085_vrc7.rs
  • crates/rustynes-mappers/src/m087_jaleco87.rs
  • crates/rustynes-mappers/src/m088_namco118.rs
  • crates/rustynes-mappers/src/m089_sunsoft2.rs
  • crates/rustynes-mappers/src/m093_sunsoft3r.rs
  • crates/rustynes-mappers/src/m094_un1rom.rs
  • crates/rustynes-mappers/src/m095_namcot3425.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs
  • crates/rustynes-mappers/src/m099_vs_system.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/m113_ave_nina006.rs
  • crates/rustynes-mappers/src/m118_txsrom.rs
  • crates/rustynes-mappers/src/m119_tqrom.rs
  • crates/rustynes-mappers/src/m132_txc_22211.rs
  • crates/rustynes-mappers/src/m136_sachen_3011.rs
  • crates/rustynes-mappers/src/m151_konami_vs.rs
  • crates/rustynes-mappers/src/m152_bandai152.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs
  • crates/rustynes-mappers/src/m177_hengedianzi.rs
  • crates/rustynes-mappers/src/m179_hengedianzi.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/m184_sunsoft1.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m210_namco175.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m240_cne_multicart.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m250_nitra250.rs
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs
  • crates/rustynes-mappers/src/m513_sachen_9602.rs
  • crates/rustynes-mappers/src/mapper.rs
  • crates/rustynes-mappers/src/mmc3_clones.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs
  • crates/rustynes-mappers/src/nsf.rs
  • crates/rustynes-mappers/src/nsf_expansion.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/sprint10.rs
  • crates/rustynes-mappers/src/sprint11.rs
  • crates/rustynes-mappers/src/sprint12.rs
  • crates/rustynes-mappers/src/sprint13.rs
  • crates/rustynes-mappers/src/sprint2.rs
  • crates/rustynes-mappers/src/sprint3.rs
  • crates/rustynes-mappers/src/sprint5.rs
  • crates/rustynes-mappers/src/sprint6.rs
  • crates/rustynes-mappers/src/sprint7.rs
  • crates/rustynes-mappers/src/sprint8.rs
  • crates/rustynes-mappers/src/sprint9.rs
  • crates/rustynes-mappers/src/tier.rs
  • crates/rustynes-mappers/src/unif.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-mappers/tests/corpus.rs
  • crates/rustynes-netplay/tests/determinism.rs
  • crates/rustynes-netplay/tests/udp_loopback.rs
  • crates/rustynes-ppu/Cargo.toml
  • crates/rustynes-ppu/src/ppu.rs
  • crates/rustynes-ppu/src/snapshot.rs
  • crates/rustynes-test-harness/Cargo.toml
  • crates/rustynes-test-harness/src/bin/pgo_trainer.rs
  • crates/rustynes-test-harness/tests/accuracycoin_runahead.rs
  • crates/rustynes-test-harness/tests/audio_expansion.rs
  • crates/rustynes-test-harness/tests/cpu_reset.rs
  • crates/rustynes-test-harness/tests/fast_dotloop_diff.rs
  • crates/rustynes-test-harness/tests/holy_mapperel.rs
  • crates/rustynes-test-harness/tests/mmc3_clone_a12.rs
  • crates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rs
  • crates/rustynes-test-harness/tests/movie.rs
  • crates/rustynes-test-harness/tests/ppu_sprites.rs
  • crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
  • crates/rustynes-test-harness/tests/visual_regression.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md
  • docs/apu-2a03.md
  • docs/compatibility.md
  • docs/expansion-audio.md
  • docs/frontend.md
  • docs/mappers.md
  • docs/performance.md
  • docs/ppu-2c02.md
  • docs/testing-strategy.md
  • scripts/agy-review.sh
  • scripts/bench_relative_check.sh
  • scripts/diag/README.md
  • scripts/diag/dma_loop_span_analyzer.py
  • scripts/diag/dma_sweep_analyzer.py
  • scripts/diag/dump_trace_csv.py
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/diag/ppu2002_diff_traces.py
  • scripts/diag/ppu2002_isolated_exact_timed_reads.py
  • scripts/diag/ppu2002_prerender_vbl_reads.py
  • scripts/diag/ppu2002_read_context_window.py
  • scripts/diag/ppu2002_read_value_histogram.py
  • scripts/diag/ppu2004_diff_vs_answerkey.py
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py
  • scripts/diag/ppu2007_fetch_bus_dump.py
  • scripts/diag/ppu2007_key_alignment_shift_scan.py
  • scripts/diag/ppu2007_offset_greedy_search.py
  • scripts/diag/ppu2007_pattern_value_sets.py
  • scripts/diag/ppu2007_phase_offset_ranking.py
  • scripts/diag/ppu2007_stress_per_index_evaluator.py
  • scripts/mapper-promotion/README.md
  • scripts/mapper-promotion/batch2.py
  • scripts/mapper-promotion/convert_gg.py
  • scripts/mapper-promotion/enumerate_staged.py
  • scripts/mapper-promotion/gen_promotion.py
  • scripts/mapper-promotion/mapper_scan.py
  • scripts/mapper-promotion/promo_tests.rs
  • scripts/mapper-promotion/promo_tests_2.rs
  • scripts/mapper-promotion/scan.py
  • scripts/perf/perf_capture.sh
  • scripts/pr-review/README.md
  • scripts/pr-review/list_all_threads.py
  • scripts/pr-review/list_unresolved_threads.py
  • scripts/release-automation/split_commit.sh
  • tests/roms/LICENSES.md
  • tests/roms/README.md
  • tests/roms/assorted/README.md
  • tests/roms/assorted/apu_01_len_ctr.nes
  • tests/roms/assorted/apu_02_len_table.nes
  • tests/roms/assorted/branch_timing_1_basics.nes
  • tests/roms/assorted/branch_timing_2_backward.nes
  • tests/roms/assorted/branch_timing_3_forward.nes
  • tests/roms/assorted/cpu_reset_ram_after_reset.nes
  • tests/roms/assorted/cpu_reset_registers.nes
  • tests/roms/assorted/cpu_timing_test.nes
  • tests/roms/assorted/flowing_palette.nes
  • tests/roms/assorted/full_palette.nes
  • tests/roms/assorted/nestest.nes
  • tests/roms/assorted/oam_read.nes
  • tests/roms/assorted/oam_stress.nes
  • tests/roms/mmc5/README.md
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md
  • to-dos/ROADMAP.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (44)
.github/release-notes/v2.2.3.md (2)

19-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the actual serde migration attribute.

The implementation uses #[serde(default = "default_fast_dotloop")]; plain #[serde(default)] would default bool to false and silently disable the fast path for older configs. Update this release note to match crates/rustynes-frontend/src/config.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 19, Update the release note’s serde
migration description to state that the [emulation] configuration uses
#[serde(default = "default_fast_dotloop")], matching the implementation in
config.rs and documenting that older configs default fast_dotloop to enabled.

73-77: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the movie/netplay compatibility explanation.

This says movies and netplay “both re-derive state from a fresh power-on,” but netplay rollback and TAS seeking use snapshot round trips and receive the v8 state fix. State the precise scope: old .rns files are rejected, .rnm playback remains accepted, and in-memory rollback/TAS snapshots use the updated schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 73 - 77, The release note’s
movie/netplay compatibility statement is inaccurate. Replace the claim that both
re-derive state from a fresh power-on with the precise scope: old .rns files are
rejected, .rnm playback remains accepted, and in-memory netplay rollback and
TAS-seeking snapshots use the updated v8 schema.
.github/workflows/pgo.yml (1)

58-74: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
printf 'Files:\n'
git ls-files '.github/workflows/*.yml' | sed -n '1,120p'
printf '\n--- pgo.yml (numbered excerpt) ---\n'
cat -n .github/workflows/pgo.yml | sed -n '1,220p'
printf '\n--- search for permissions/workflow_call/callers ---\n'
rg -n 'workflow_call|permissions:|uses: .*pgo\.yml|pgo\.yml@|\.github/workflows/pgo\.yml' .github/workflows

Repository: doublegate/RustyNES

Length of output: 2111


🌐 Web query:

GitHub Actions reusable workflow permissions inheritance GITHUB_TOKEN caller workflow_call docs

💡 Result:

In GitHub Actions, GITHUB_TOKEN permissions do not automatically inherit from a caller workflow to a reusable workflow [1][2]. Instead, the following rules govern permissions in reusable workflow scenarios: 1. Independent Permission Evaluation: The GITHUB_TOKEN permissions for a reusable workflow are determined by its own permissions declaration (if present) or the repository's default permissions [3]. The permissions block specified in the caller's job does not propagate or apply to the called workflow [3]. 2. Downgrading Restriction: While permissions are not inherited, they are subject to a hierarchy of restrictions across the workflow chain [1][2]. The GITHUB_TOKEN permissions in a called (reusable) workflow can only be equal to or more restrictive than the permissions defined for the caller's job [1][2]. You cannot elevate permissions in a called workflow beyond what is granted in the caller [1][2]. 3. Nested Workflows: For chains of reusable workflows (e.g., A > B > C), this restriction applies throughout the chain [1][2][4]. Workflow C cannot have broader permissions than workflow B, which in turn cannot have broader permissions than workflow A [1][2]. 4. Default Behavior: If no permissions are specified in the reusable workflow, it will utilize the repository's default GITHUB_TOKEN permissions [1][2]. To ensure a reusable workflow has the necessary access, you must explicitly define the required permissions within the reusable workflow's own configuration [3]. If you attempt to grant permissions in the called workflow that exceed those in the calling job, the workflow execution will be restricted to the caller's maximum defined permissions [1][2].

Citations:


Security Misconfiguration (CWE-250)

Reachability: External

Add an explicit read-only permissions block here
Reusable workflows do not inherit the caller job’s GITHUB_TOKEN scopes; this workflow uses its own permissions or the repository default. Since pgo runs build scripts, benchmarks, and tests, set permissions: { contents: read } on the workflow or the pgo job and keep any write access confined to the publish job.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pgo.yml around lines 58 - 74, Add an explicit read-only
permissions block with contents: read to the reusable workflow or the pgo job in
.github/workflows/pgo.yml. Ensure build, benchmark, and test steps retain
required repository access, while any write permissions remain limited to the
publish job.
AGENTS.md (1)

188-188: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale “this release” wording.

Line 188 correctly identifies v2.2.3 as current, but later labels v2.2.2 “this release.” Change that phrase to “the prior release” so future agents are not given contradictory version guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 188, Update the release-history wording in AGENTS.md so
the v2.2.2 “Conduit” reference says “the prior release” instead of “this
release.” Preserve the surrounding version chronology and all other release
descriptions unchanged.
CHANGELOG.md (1)

37-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the reported test count.

CHANGELOG.md reports 2219 passed for the whole --features test-roms suite, while .github/release-notes/v2.2.3.md reports 2237 for cargo test --workspace --features test-roms. Keep one count, or explicitly identify why these are different subsets, so the release artifacts do not publish contradictory verification results.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 37 - 40, Reconcile the test totals reported in the
CHANGELOG entry with the corresponding count in the v2.2.3 release notes. Update
the affected verification summary to use one consistent count, or explicitly
label the differing command/subset and explain the distinction so both release
artifacts accurately describe their results.
crates/rustynes-core/src/bus.rs (2)

1631-1654: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the copied expansion-device contract from this setter.

Lines 1631-1637 document set_expansion_device and a port panic, but are attached to set_zapper_temporal_light. The generated API docs are contradictory; the proper expansion-device docs already appear at Lines 1664-1670.

As per coding guidelines, public Rust APIs require accurate rationale and behavior documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/bus.rs` around lines 1631 - 1654, Remove the copied
expansion-device and port panic documentation immediately before
set_zapper_temporal_light, leaving only documentation describing the temporal
Zapper light model, its default-off behavior, and rationale. Keep the correct
expansion-device contract with set_expansion_device where it already appears.

Source: Coding guidelines


2503-2507: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not map the pre-render scanline to visible scanline zero.

Ppu::scanline() is -1 on the pre-render line, but unwrap_or(0) turns it into scanline 0. A Zapper aimed at y == 0 can therefore report light before the beam begins painting the visible frame. Return the temporal no-light byte for negative scanlines instead.

Proposed fix
-            let sl = u16::try_from(self.ppu.scanline()).unwrap_or(0);
+            let Ok(sl) = u16::try_from(self.ppu.scanline()) else {
+                return (u8::from(z.trigger_raw()) << 4) | (1 << 3);
+            };
             return z.read_at_scanline(self.ppu.framebuffer(), sl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if self.zapper_temporal_light
            && let Some(crate::input_device::InputDevice::Zapper(z)) = &self.expansion_device[port]
        {
            let Ok(sl) = u16::try_from(self.ppu.scanline()) else {
                return (u8::from(z.trigger_raw()) << 4) | (1 << 3);
            };
            return z.read_at_scanline(self.ppu.framebuffer(), sl);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/bus.rs` around lines 2503 - 2507, Update the zapper
temporal-light branch in the bus read path to handle the signed result of
Ppu::scanline() explicitly: return the temporal no-light byte for negative
scanlines, and only convert nonnegative scanlines to u16 before calling
Zapper::read_at_scanline. Remove the unwrap_or(0) fallback so the pre-render
line is never treated as visible scanline zero.
crates/rustynes-mappers/src/jaleco_discrete.rs (1)

52-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove orphaned section-divider comments left over from the module split.

These four banner comments describe boards that are not in this file — L52-58 "Mapper 38 — Bit Corp UNL-PCI556", L308-322 "Mapper 41 — Caltron 6-in-1", L631-641 "Mapper 77 — Irem (Napoleon Senki)", L756-766 "Mapper 218 — Magic Floor" — yet each precedes an unrelated Jaleco struct (86/72/101/tests). They appear to be residue from the sprint-file split and misdescribe the code that follows. Delete or replace them with the correct Jaleco 86/140/72/92/101 headers.

Also applies to: 308-322, 631-641, 756-766

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs` around lines 52 - 58, Remove
the four orphaned mapper banner comment blocks before the unrelated Jaleco
structs, or replace them with accurate headers for the following Mapper 86, 72,
101, and test-related code. Ensure no comments incorrectly describe Mapper 38,
41, 77, or 218 in this file.
crates/rustynes-mappers/src/m009_mmc2.rs (1)

277-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate #[cfg(test)] on the tests module. The attribute is applied twice in three new files — a copy-paste artifact with no behavioral effect that no default clippy/rustfmt lint removes. Drop the redundant line at each site.

  • crates/rustynes-mappers/src/m009_mmc2.rs#L277-L278: remove one of the two #[cfg(test)] lines.
  • crates/rustynes-mappers/src/m021_vrc4.rs#L461-L462: remove one of the two #[cfg(test)] lines.
  • crates/rustynes-mappers/src/m022_vrc2.rs#L350-L351: remove one of the two #[cfg(test)] lines.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m009_mmc2.rs#L277-L278 (this comment)
  • crates/rustynes-mappers/src/m021_vrc4.rs#L461-L462
  • crates/rustynes-mappers/src/m022_vrc2.rs#L350-L351
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m009_mmc2.rs` around lines 277 - 278, Remove one
redundant #[cfg(test)] attribute at the tests module in
crates/rustynes-mappers/src/m009_mmc2.rs lines 277-278,
crates/rustynes-mappers/src/m021_vrc4.rs lines 461-462, and
crates/rustynes-mappers/src/m022_vrc2.rs lines 350-351, leaving one attribute at
each site.
crates/rustynes-mappers/src/m032_irem_g101.rs (1)

125-148: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

bank_count - 2 underflows on a single-bank PRG-ROM. The constructor accepts any non-zero 8 KiB multiple, so bank_count == 1 is reachable from a crafted iNES image. In default mode 0 the first $C000-$DFFF read evaluates bank_count - 2, which underflows: overflow-checked builds panic on that untrusted-ROM path, and release only stays in bounds because the wrap survives the later % bank_count. Use saturating_sub(2) to match the fixed-tail idiom already in m088_namco118.rs.

As per coding guidelines: outside #[cfg(test)], do not introduce panics on untrusted data parsed from ROMs in the #![no_std] chip stack.

Proposed fix
         let bank = match slot {
             0 => {
                 if self.prg_swap_mode {
-                    bank_count - 2 // mode 1: $8000 fixed {-2}
+                    bank_count.saturating_sub(2) // mode 1: $8000 fixed {-2}
                 } else {
                     self.prg_bank[0] as usize // mode 0: $8000 swappable
                 }
             }
             1 => self.prg_bank[1] as usize, // $A000 always swappable
             2 => {
                 if self.prg_swap_mode {
                     self.prg_bank[0] as usize // mode 1: $C000 swappable
                 } else {
-                    bank_count - 2 // mode 0: $C000 fixed {-2}
+                    bank_count.saturating_sub(2) // mode 0: $C000 fixed {-2}
                 }
             }
             _ => bank_count - 1, // $E000 always fixed {-1}
         } % bank_count;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn read_prg(&self, addr: u16) -> u8 {
        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
        let bank = match slot {
            0 => {
                if self.prg_swap_mode {
                    bank_count.saturating_sub(2) // mode 1: $8000 fixed {-2}
                } else {
                    self.prg_bank[0] as usize // mode 0: $8000 swappable
                }
            }
            1 => self.prg_bank[1] as usize, // $A000 always swappable
            2 => {
                if self.prg_swap_mode {
                    self.prg_bank[0] as usize // mode 1: $C000 swappable
                } else {
                    bank_count.saturating_sub(2) // mode 0: $C000 fixed {-2}
                }
            }
            _ => bank_count - 1, // $E000 always fixed {-1}
        } % bank_count;
        let off = (addr as usize) & (PRG_BANK_8K - 1);
        self.prg_rom[bank * PRG_BANK_8K + off]
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m032_irem_g101.rs` around lines 125 - 148, Update
the fixed-bank calculations in read_prg, specifically both bank_count - 2
expressions for the $8000/$C000 slots, to use saturating subtraction so
single-bank PRG-ROMs cannot underflow. Preserve the existing modulo bank_count
mapping and all other slot behavior.

Source: Coding guidelines

crates/rustynes-mappers/src/m036_txc_policeman.rs (2)

6-6: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

A crate-doc paragraph break was collapsed into text.//!, breaking rustdoc rendering. Each site ends a //! line with a trailing //! that was meant to be the next (empty) doc line; split it onto its own line so the paragraph break renders.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6: split chip.//! into chip. then a blank //! line.
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7: split register.//! into register. then a blank //! line.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L6-L6: split load-bearing.//! into load-bearing. then a blank //! line.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6 (this comment)
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L6-L6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` at line 6, Restore the
crate-doc paragraph breaks by splitting the collapsed trailing doc markers: in
crates/rustynes-mappers/src/m036_txc_policeman.rs lines 6-6,
crates/rustynes-mappers/src/m095_namcot3425.rs lines 7-7, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs lines 6-6, keep each
sentence-ending period on its own `//!` line and add the following blank `//!`
line.

53-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Orphaned // === Mapper NN === section comments left by the module split describe mappers not present in their files. When the combined module was split into board-named files, each file kept comment blocks (and duplicate #[cfg(test)] attributes) belonging to neighboring sections. These now actively misdescribe the file contents. Delete the orphaned blocks (and the redundant second #[cfg(test)]); keep only the block for the mapper each file actually implements.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L71: drop the "Mapper 15" block (and the "Mapper 39" block at L189-196; remove the duplicate #[cfg(test)] at L198-199).
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167: drop the "Mapper 79" block.
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52: drop the "Mapper 15" block (and the "Mapper 61" block at L192-203).
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71: drop the "Mapper 28" block (and the "Mapper 112" block at L234-249 — but first resolve the missing-implementation verification raised on that file; remove the duplicate #[cfg(test)] at L251-252).
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56: drop the "Mapper 15" block (and the "Mapper 97" block at L237-244).
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65: drop the "Mapper 15" block (and the "Mapper 132" block at L221-233; remove the duplicate #[cfg(test)] at L235-236).
📍 Affects 6 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L71 (this comment)
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` around lines 53 - 71,
Remove the orphaned mapper comment blocks and redundant duplicate cfg(test)
attributes across the listed mapper files, retaining only each file’s own mapper
documentation. In m036_txc_policeman.rs remove the Mapper 15 and Mapper 39
blocks plus the duplicate cfg(test); in m038_bitcorp38.rs remove Mapper 79; in
m039_subor39.rs remove Mapper 15 and Mapper 61; in m095_namcot3425.rs resolve
the missing-implementation verification first, then remove Mapper 28, Mapper
112, and the duplicate cfg(test); in m096_bandai96.rs remove Mapper 15 and
Mapper 97; and in m097_irem_tam_s1.rs remove Mapper 15, Mapper 132, and the
duplicate cfg(test).
crates/rustynes-mappers/src/m077_irem_napoleon.rs (1)

222-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant #[cfg(test)] attribute repeated on the tests module. The attribute is duplicated on consecutive lines — harmless but a copy-paste template artifact carried into three new modules. Drop the second one in each.

  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L222-L223: remove the duplicate #[cfg(test)] above mod tests.
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227: remove the duplicate #[cfg(test)] above mod tests.
  • crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185: remove the duplicate #[cfg(test)] above mod tests.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L222-L223 (this comment)
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227
  • crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m077_irem_napoleon.rs` around lines 222 - 223,
Remove the second consecutive #[cfg(test)] attribute above mod tests in
crates/rustynes-mappers/src/m077_irem_napoleon.rs:222-223,
crates/rustynes-mappers/src/m079_ave_nina03_06.rs:226-227, and
crates/rustynes-mappers/src/m094_un1rom.rs:184-185, leaving one attribute per
tests module.
crates/rustynes-mappers/src/m085_vrc7.rs (1)

535-632: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the OPLL core expose serialization, and does the schema audit cover mappers?
fd -e rs . crates/rustynes-apu/src | xargs rg -nP '\b(pub\s+)?(fn|struct)\s+Opll\b|fn\s+(snapshot|save_state|serialize|restore|load_state|deserialize)\b' -C1
rg -nP 'mix_audio|Opll|mapper|save_state' crates/rustynes-test-harness/tests/snapshot_schema_audit.rs -C2 || true

Repository: doublegate/RustyNES

Length of output: 2140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files 'crates/rustynes-mappers/src/m085_vrc7.rs' \
             'crates/rustynes-apu/src/**' \
             'crates/rustynes-test-harness/tests/snapshot_schema_audit.rs' || true

echo "== map relevant symbols =="
ast-grep outline crates/rustynes-mappers/src/m085_vrc7.rs --view expanded || true
echo "--- apu files ---"
fd -a -e rs crates/rustynes-apu/src || true

echo "== search for Opll serialization and state fields =="
rg -n --hidden --no-messages -S 'struct Opll|impl Opll|fn (save_state|load_state|serialize|deserialize|snapshot|restore)|opll_clock_counter|last_opll_sample|audio\.regs|self\.opll' crates/rustynes-apu/src crates/rustynes-mappers/src/m085_vrc7.rs crates/rustynes-test-harness/tests/snapshot_schema_audit.rs || true

echo "== inspect relevant ranges =="
for f in crates/rustynes-mappers/src/m085_vrc7.rs crates/rustynes-test-harness/tests/snapshot_schema_audit.rs; do
  if [ -f "$f" ]; then
    echo "--- $f (lines around relevant area) ---"
    nl -ba "$f" | sed -n '500,700p'
  fi
done

echo "== apu outline candidates =="
for f in $(git ls-files 'crates/rustynes-apu/src/**' | tr '\n' ' '); do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded || true
done

Repository: doublegate/RustyNES

Length of output: 2111


Persist the live VRC7 OPLL state in snapshots crates/rustynes-mappers/src/m085_vrc7.rs:535-632

save_state only writes the shadow register bytes here; load_state never restores the live self.opll state or the clock/sample counters. That leaves VRC7 audio out of sync after rewind/netplay/TAS restore, so snapshotting needs to include the synthesizer state or rebuild it from the serialized fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m085_vrc7.rs` around lines 535 - 632, Update the
VRC7 snapshot methods save_state and load_state to preserve and restore the live
self.opll synthesizer state together with its clock/sample counters, rather than
serializing only self.audio.regs. Bump the snapshot version and append the OPLL
state to the format, or explicitly rebuild self.opll from the restored
serialized fields, ensuring restored audio remains synchronized after loading.

Source: Coding guidelines

crates/rustynes-mappers/src/m093_sunsoft3r.rs (1)

106-112: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== m093_sunsoft3r.rs ==\n'
cat -n crates/rustynes-mappers/src/m093_sunsoft3r.rs | sed -n '1,180p'

printf '\n== m089_sunsoft2.rs ==\n'
cat -n crates/rustynes-mappers/src/m089_sunsoft2.rs | sed -n '1,180p'

printf '\n== m094_un1rom.rs ==\n'
cat -n crates/rustynes-mappers/src/m094_un1rom.rs | sed -n '1,220p'

Repository: doublegate/RustyNES

Length of output: 2111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== m093_sunsoft3r.rs ==\n'
sed -n '1,180p' crates/rustynes-mappers/src/m093_sunsoft3r.rs | cat -n

printf '\n== m089_sunsoft2.rs ==\n'
sed -n '1,180p' crates/rustynes-mappers/src/m089_sunsoft2.rs | cat -n

printf '\n== m094_un1rom.rs ==\n'
sed -n '1,220p' crates/rustynes-mappers/src/m094_un1rom.rs | cat -n

Repository: doublegate/RustyNES

Length of output: 2111


🌐 Web query:

Sunsoft 3R mapper 93 bus conflict PRG-ROM mask nesdev

💡 Result:

The Sunsoft-2 integrated circuit, used on the Sunsoft-3R board (iNES Mapper 93), is subject to bus conflicts when writing to its registers [1][2]. In NES and Famicom cartridge development, a bus conflict occurs when the mapper register is mapped to the same address space as the PRG-ROM [2]. When the CPU attempts to write to a mapper register, the data output from the CPU conflicts with the data being output by the PRG-ROM chip at that same address [2]. For the Sunsoft-2 mapper: - The mapper registers are located in the $8000-$FFFF address range [1][3]. - When writing to these registers, the value being written must be logically ANDed with the data stored at the target address in the PRG-ROM [2]. This "bus conflict" behavior is a physical side effect of the mapper's design, as the mapper does not disable the PRG-ROM output during writes [2]. Emulators must replicate this behavior—often referred to as 'accounting for bus conflicts'—to ensure compatibility with original software, as some games may rely on this characteristic [2]. The Sunsoft-3R board is otherwise identical to the Sunsoft-3 board (Mapper 89), differing only in jumper configurations to support different memory types (CHR-RAM vs. CHR-ROM) [4][5][6].

Citations:


🌐 Web query:

NESDev mapper 93 Sunsoft 3R bus conflicts

💡 Result:

The Sunsoft-3R board, which uses the Sunsoft-2 IC (assigned iNES Mapper 093), is subject to bus conflicts [1][2]. In the context of the NES, bus conflicts occur when the CPU attempts to write to a register while the mapper's internal hardware and the PRG ROM are both driving the data bus simultaneously [2]. For Mapper 93, the Sunsoft-2 IC registers reside within the $8000-$FFFF range [1][3]. Because the registers are implemented using standard TTL components (specifically a 74377 latch or equivalent functionality within the custom Sunsoft-2 IC), the register is mapped over the same address space as the PRG ROM [1][4]. When the CPU performs a write instruction to any address in the $8000-$FFFF range, both the mapper and the PRG ROM may attempt to drive the bus [2]. To avoid bus conflicts, software must ensure that the value being written to the register is identical to the data already present at that address in the PRG ROM [2]. This technique is commonly used for many NES mappers that exhibit bus conflicts, such as the MMC3 or standard ROM-based bankswitching schemes, ensuring that the signal on the bus remains stable regardless of whether the ROM or the register is responding [2]. In emulation, accounts of Mapper 93 often note the presence of these conflicts, and accurate emulators must simulate this behavior, typically by having the register read the data from the PRG ROM at the target address before completing the write operation to the internal register, or by otherwise accounting for the bus conflict to avoid logic errors [2][5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
for f in \
  crates/rustynes-mappers/src/m093_sunsoft3r.rs \
  crates/rustynes-mappers/src/m089_sunsoft2.rs \
  crates/rustynes-mappers/src/m094_un1rom.rs
do
  echo "== $f =="
  nl -ba "$f" | sed -n '1,220p'
done

Repository: doublegate/RustyNES

Length of output: 2111


Apply the bus-conflict mask before decoding this register. Mapper 93 is bus-conflict affected, so value should be ANDed with self.cpu_read(addr) before extracting the PRG bank and CHR-RAM bits; otherwise writes can select the wrong bank.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m093_sunsoft3r.rs` around lines 106 - 112, Update
M093’s cpu_write method to apply the bus-conflict mask by ANDing the incoming
value with self.cpu_read(addr) before decoding the register. Use the masked
value for both prg_bank and chr_ram_enabled extraction, while preserving the
existing 0x8000..=0xFFFF address guard.
crates/rustynes-mappers/src/m107_magic_dragon107.rs (1)

40-47: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Stale section-divider comments describe unrelated mappers. The per-file split left behind block comments for other mappers that no longer live in these files; they actively misdescribe the code (e.g. an m107 file headed "Mapper 31"). Delete or rewrite each to match the module it now sits in.

  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47: remove the "Mapper 31 — INL / NSF-style" divider (file is mapper 107).
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L168-L175: remove the "Mapper 143 — Sachen TCA01" divider (no such impl here).
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54: remove the "Mapper 38 — Bit Corp UNL-PCI556" divider.
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L208-L217: remove the "Mapper 86 — Jaleco JF-13" divider.
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70: remove the "Mapper 15 — K-1029" divider.
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275: remove the "Mapper 133 — Sachen 3009" divider.
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315: remove the "SimpleBmc" divider (unrelated BMC boards).
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58: remove the "Mapper 40 — NTDEC 2722" divider.
  • crates/rustynes-mappers/src/m156_daou156.rs#L229-L244: remove the "Mapper 162 — Waixing FS304" divider.
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59: remove the "Mapper 31" divider.
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L182-L188: remove the "Mapper 179" divider (belongs to the sibling file).
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60: remove the "Mapper 31" divider.
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L183-L193: remove the "Mapper 58 — multicart" divider.
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58: remove the "Mapper 147 — Sachen 3018" divider.
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L215-L229: remove the "Mapper 185 — CNROM" divider.
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62: remove the "Mapper 147 — Sachen 3018" divider.
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L234-L245: remove the "Mapper 200 — MG109" divider.

As per coding guidelines: "Provide comprehensive //! crate/module documentation and /// or // comments explaining rationale ...".

📍 Affects 9 files
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47 (this comment)
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L168-L175
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L208-L217
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58
  • crates/rustynes-mappers/src/m156_daou156.rs#L229-L244
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L182-L188
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L183-L193
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L215-L229
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L234-L245
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs` around lines 40 - 47,
Remove the stale unrelated section-divider comments at
crates/rustynes-mappers/src/m107_magic_dragon107.rs:40-47 and :168-175;
crates/rustynes-mappers/src/m113_ave_nina006.rs:48-54 and :208-217;
crates/rustynes-mappers/src/m132_txc_22211.rs:58-70 and :268-275;
crates/rustynes-mappers/src/m136_sachen_3011.rs:306-315;
crates/rustynes-mappers/src/m156_daou156.rs:42-58 and :229-244;
crates/rustynes-mappers/src/m177_hengedianzi.rs:52-59 and :182-188;
crates/rustynes-mappers/src/m179_hengedianzi.rs:53-60 and :183-193;
crates/rustynes-mappers/src/m180_nichibutsu180.rs:42-58 and :215-229; and
crates/rustynes-mappers/src/m185_cnrom185.rs:46-62 and :234-245. Delete each
misleading mapper or board divider without changing the mapper implementations
or replacing it with unrelated documentation.

Source: Coding guidelines

crates/rustynes-mappers/src/m113_ave_nina006.rs (2)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapsed doc-paragraph break renders a literal //!. Each of these //! lines ends with .//!, so the intended blank-line paragraph separator was lost and rustdoc renders //! as prose. Insert an empty //! line before the "best-effort (Tier-2)" paragraph.

  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L6-L6: split window.//! into window. + a blank //! line.
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L11-L11: split m136_sachen_3011.rs.//!` into two lines.
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L5-L5: split two windows.//! into two lines.
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L6-L6: split ordinary ROM write.//! into two lines.
📍 Affects 4 files
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L6-L6 (this comment)
  • crates/rustynes-mappers/src/m136_sachen_3011.rs#L11-L11
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L5-L5
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L6-L6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs` at line 6, Fix the collapsed
rustdoc paragraph breaks by separating the prose from the literal `//!` marker
in crates/rustynes-mappers/src/m113_ave_nina006.rs:6-6,
crates/rustynes-mappers/src/m136_sachen_3011.rs:11-11,
crates/rustynes-mappers/src/m177_hengedianzi.rs:5-5, and
crates/rustynes-mappers/src/m179_hengedianzi.rs:6-6; end each preceding sentence
on its own line and add an empty `//!` line before the “best-effort (Tier-2)”
paragraph.

219-220: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated #[cfg(test)] on the test module. The split left two identical #[cfg(test)] attributes stacked on the tests module; it compiles but is redundant. Drop the duplicate in each.

  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L219-L220: keep one #[cfg(test)].
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L277-L278: keep one #[cfg(test)].
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L190-L191: keep one #[cfg(test)].
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L195-L196: keep one #[cfg(test)].
📍 Affects 4 files
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L219-L220 (this comment)
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L277-L278
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L190-L191
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L195-L196
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs` around lines 219 - 220,
Remove the duplicate #[cfg(test)] attribute above the tests module, retaining
exactly one in crates/rustynes-mappers/src/m113_ave_nina006.rs lines 219-220,
crates/rustynes-mappers/src/m132_txc_22211.rs lines 277-278,
crates/rustynes-mappers/src/m177_hengedianzi.rs lines 190-191, and
crates/rustynes-mappers/src/m179_hengedianzi.rs lines 195-196.
crates/rustynes-mappers/src/m136_sachen_3011.rs (1)

206-209: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Out-of-bounds PRG index panics on a valid ROM image. check_prg (Line 80) admits any non-zero multiple of 8 KiB, but this read takes the modulus against count * PRG_BANK_32K (32768 when count == 1), not the actual image length. For a 16 KiB PRG, addr = 0xC000 yields (0x4000) % 32768 = 16384, indexing one past a 16 KiB image → panic. This is reachable from ROM-parsed (untrusted) data and defeats the crate's stated "wraps with % count, so a register write can never index out of bounds" invariant.

Wrap against the actual image length (or tighten check_prg to require exactly 32 KiB for mapper 136):

Proposed fix
             0x8000..=0xFFFF => {
-                let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
-                self.prg_rom[(addr as usize & 0x7FFF) % (count * PRG_BANK_32K)]
+                self.prg_rom[(addr as usize & 0x7FFF) % self.prg_rom.len()]
             }
As per coding guidelines: "do not add `.unwrap()`, `.expect()`, or `panic!()` when applied to untrusted data parsed from ROMs ... Return a typed `Result` or error instead."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            0x8000..=0xFFFF => {
                self.prg_rom[(addr as usize & 0x7FFF) % self.prg_rom.len()]
            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs` around lines 206 - 209,
Update the PRG read branch for 0x8000..=0xFFFF to wrap the computed offset
against self.prg_rom.len(), not count * PRG_BANK_32K, so every valid non-empty
ROM length accepted by check_prg remains in bounds. Preserve the existing
bank-address calculation and avoid introducing unwrap, expect, or panic handling
for ROM-derived data.

Source: Coding guidelines

crates/rustynes-mappers/src/m232_camerica_bf9096.rs (1)

38-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove stale orphan "Mapper NN" section-divider comments. Each of these blocks documents the hardware behavior of a different mapper than the file it lives in — copy-paste residue from the module split — and will mislead maintainers reading a single-board module.

  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44: delete the "Mapper 38 — Bit Corp UNL-PCI556" block.
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L191-L200: delete the "Mapper 240 — C&E multicart" block.
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53: delete the "Mapper 38 — Bit Corp UNL-PCI556" block.
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183: delete the "Mapper 241 — BxROM-like pirate" block.
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41: delete the "Mapper 38 — Bit Corp UNL-PCI556" block.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53: delete the "Mapper 38 — Bit Corp UNL-PCI556" block.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207: delete the "Mapper 250 — Nitra" block.
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L42-L59: delete the "Mapper 28 — Action 53" block.
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58: delete the "Mapper 40 — NTDEC 2722" block.
📍 Affects 6 files
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44 (this comment)
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L191-L200
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L42-L59
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m232_camerica_bf9096.rs` around lines 38 - 44,
Remove the stale orphan mapper section-divider comment blocks, without changing
surrounding implementation: delete the specified blocks in
crates/rustynes-mappers/src/m232_camerica_bf9096.rs at 38-44 and 191-200;
crates/rustynes-mappers/src/m240_cne_multicart.rs at 47-53 and 177-183;
crates/rustynes-mappers/src/m241_bxrom241.rs at 35-41;
crates/rustynes-mappers/src/m244_cne_decathlon.rs at 47-53 and 195-207;
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs at 42-59; and
crates/rustynes-mappers/src/m250_nitra250.rs at 42-58.
crates/rustynes-mappers/src/m240_cne_multicart.rs (1)

5-5: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix copy-paste artifacts in the module headers. Two mechanical slips: a doc paragraph break was collapsed into the preceding line (rendering a literal //!), and the #[cfg(test)] attribute is duplicated on the test module.

  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L5-L5: split ...also be using.//! into ...also be using. followed by a separate //! blank doc line.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L5-L5: split ...bank number directly.//! into ...bank number directly. followed by a separate //! blank doc line.
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186: remove the duplicate #[cfg(test)].
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210: remove the duplicate #[cfg(test)].
📍 Affects 2 files
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L5-L5 (this comment)
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L5-L5
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs` at line 5, Fix the
module-header formatting in
crates/rustynes-mappers/src/m240_cne_multicart.rs:5-5 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs:5-5 by placing the trailing
//! on its own blank documentation line. Remove the duplicate #[cfg(test)]
attribute in crates/rustynes-mappers/src/m240_cne_multicart.rs:185-186 and
crates/rustynes-mappers/src/m244_cne_decathlon.rs:209-210, leaving one attribute
for each test module.
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs (1)

8-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring tier classification contradicts tier.rs. Both modules describe themselves as "best-effort (Tier-2)" and point at MapperTier::BestEffort, but tier.rs places ids 246 and 250 in CURATED_IDS and its classifier arm returns MapperTier::Curated (asserted by all_curated_ids_classify_as_curated). The tested classifier is authoritative; the docstrings drifted.

  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L8-L15: change "best-effort (Tier-2)" / MapperTier::BestEffort to Curated (Tier-1).
  • crates/rustynes-mappers/src/m250_nitra250.rs#L9-L16: change "best-effort (Tier-2)" / MapperTier::BestEffort to Curated (Tier-1).
📍 Affects 2 files
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L8-L15 (this comment)
  • crates/rustynes-mappers/src/m250_nitra250.rs#L9-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m246_fong_shen_bang246.rs` around lines 8 - 15,
Update the module docstrings in
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs lines 8-15 and
crates/rustynes-mappers/src/m250_nitra250.rs lines 9-16 to classify both mappers
as Curated (Tier-1), replacing the BestEffort/Tier-2 wording and
MapperTier::BestEffort reference while leaving implementation behavior
unchanged.
crates/rustynes-mappers/src/m250_nitra250.rs (1)

127-137: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

last - 1 underflows on a single-bank PRG image.

new() accepts any non-zero multiple of 8 KiB, so an 8 KiB PRG gives last == 0. The arm (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1 then computes 0usize - 1, which panics in debug builds on a normal $C000 read. Release builds are saved only by the later % count in read_prg. This contradicts the module's own "cannot afford a panic on a register access" invariant; m004_mmc3 avoids it with saturating_sub.

Proposed fix
-        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
+        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
+        let second_last = last.saturating_sub(1);
         let r6 = self.bank_regs[6] as usize;
         let r7 = self.bank_regs[7] as usize;
         match (self.prg_mode, addr) {
             (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
-            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1,
+            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => second_last,
             (_, 0xA000..=0xBFFF) => r7,
             _ => last,
         }
As per coding guidelines: outside `#[cfg(test)]`, do not add code that can `panic!()` on data parsed from ROMs; return/compute safely instead.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn prg_bank_for(&self, addr: u16) -> usize {
        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        let second_last = last.saturating_sub(1);
        let r6 = self.bank_regs[6] as usize;
        let r7 = self.bank_regs[7] as usize;
        match (self.prg_mode, addr) {
            (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => second_last,
            (_, 0xA000..=0xBFFF) => r7,
            _ => last,
        }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m250_nitra250.rs` around lines 127 - 137, Update
prg_bank_for so the fixed-bank expression for the (false, 0xC000..=0xDFFF) and
(true, 0x8000..=0x9FFF) arms cannot underflow when last is zero. Use saturating
subtraction, matching the safe handling in m004_mmc3, while preserving the
existing bank selection for larger PRG images.

Source: Coding guidelines

crates/rustynes-mappers/src/ntdec.rs (1)

60-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reorg left orphaned // === section-header comment blocks describing mappers that are no longer the code they precede. The sprint→mNNN_* module reorganization relocated the implementations but not their descriptive headers, so each header now documents an unrelated mapper. The underlying banking/IRQ/save-state logic is correct in every case; this is purely stale documentation, which is worse than none in a #![no_std] chip crate. Delete or rewrite each block to match the code that follows:

  • crates/rustynes-mappers/src/ntdec.rs#L60-L78: header says "Mapper 28 — Action 53" but precedes Ntdec63; also fix the "Mapper 76" (L246), "Mapper 225" (L411), "Mapper 95" (L727), "Mapper 137" (L901), "Bmc204/204" (L1123), and "Bmc11160/299" (L1327) blocks to match Ntdec174/mapper-40/NtdecAsder112/NtdecTc112(193)/NtdecN625092(221)/test-module respectively.
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74: "Mapper 40 — NTDEC 2722" precedes Sachen8259M137; also fix "Mapper 156 — DIS23C01 DAOU" (L248) before the 8259 A/B/C core and "Mapper 42" (L482) before the test module.
  • crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74: "Mapper 15 — K-1029" precedes Sachen133; also fix "Mapper 180" (L1273) before SachenTca01M143 and "Mapper 177" (L1415) before the test module.
  • crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedes Waixing242; also fix "Mapper 246" (L270) before WaixingFs304M162 and "Mapper 244" (L647) before Waixing253.
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344: "TxcChip … Sachen 3011 / m136" block precedes the test module; there is no TxcChip in this file.
  • crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780: "Sachen 8259 (A/B/C)" block precedes the test module; that family lives in sachen_8259.rs.

As per coding guidelines: "Provide comprehensive //! crate/module documentation and /// or // comments explaining rationale".

📍 Affects 6 files
  • crates/rustynes-mappers/src/ntdec.rs#L60-L78 (this comment)
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74
  • crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74
  • crates/rustynes-mappers/src/waixing.rs#L63-L81
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344
  • crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/ntdec.rs` around lines 60 - 78, Replace or remove
the stale mapper header blocks so each comment documents the implementation or
test module that follows, without changing banking, IRQ, or save-state logic. In
crates/rustynes-mappers/src/ntdec.rs at lines 60-78, 246, 411, 727, 901, 1123,
and 1327, update headers for Ntdec63, Ntdec174, mapper-40, NtdecAsder112,
NtdecTc112 (193), NtdecN625092 (221), and the test module respectively. Apply
the corresponding corrections in crates/rustynes-mappers/src/sachen_8259.rs at
lines 58-74, 248, and 482; crates/rustynes-mappers/src/sachen_discrete.rs at
lines 62-74, 1273, and 1415; crates/rustynes-mappers/src/waixing.rs at lines
63-81, 270, and 647; remove or rewrite the nonexistent TxcChip header in
crates/rustynes-mappers/src/m513_sachen_9602.rs at lines 341-344; and correct
the misplaced Sachen 8259 header in crates/rustynes-mappers/src/mmc3_clones.rs
at lines 767-780 before its test module.

Source: Coding guidelines

docs/performance.md (2)

692-696: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the remaining-dot breakdown.

The stated 20,400 count includes dot 0 on each visible scanline, but the text labels it only as dots 257..=340; it also omits the post-render line and therefore does not explain the stated 27,902 remainder. Use the exact breakdown: visible-line dot 0 plus dots 257..=340 = 20,400, post-render line 240 = 341, vblank lines 241–260 = 6,820, and pre-render line 261 = 341.

As per path instructions, documentation is the specification and must remain synchronized with code-accurate behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/performance.md` around lines 692 - 696, Correct the remaining-dot
breakdown in the performance documentation to explicitly include visible-line
dot 0 alongside dots 257..=340 for 20,400 dots, then list post-render line 240
as 341, vblank lines 241–260 as 6,820, and pre-render line 261 as 341,
preserving the 27,902 total.

Source: Path instructions


1049-1050: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a current release-tag example.

The added instructions are correct, but the immediately following command still pushes v1.2.0, an obsolete tag that cannot serve as a valid example for the v2.2.3 release flow. Replace it with a current or clearly future placeholder tag.

As per path instructions, user-facing documentation must provide correct operational instructions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/performance.md` around lines 1049 - 1050, Update the release-tag command
immediately following the documented release.yml flow to replace the obsolete
v1.2.0 example with the current v2.2.3 tag or a clearly future placeholder,
keeping the surrounding release instructions unchanged.

Source: Path instructions

docs/ppu-2c02.md (1)

242-265: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the v8 compatibility consequence.

This section explains the new serialized fields but does not state that the v8 schema is a compatibility epoch: existing v2.2.2-and-earlier .rns files must be rejected with a version error rather than upconverted. Add that caller-visible behavior here or explicitly link to the authoritative compatibility policy.

As per path instructions and ADR 0028, save-state schema changes must document their version-guard and compatibility behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ppu-2c02.md` around lines 242 - 265, The save-state coverage section
must document v8 as a compatibility epoch: state files from v2.2.2 and earlier
must be rejected with a version error rather than upconverted. Add this
caller-visible behavior near the PPU_SNAPSHOT_VERSION v8 discussion, or link
directly to the authoritative compatibility policy required by ADR 0028.

Source: Path instructions

scripts/diag/locate_first_trace_divergence.py (1)

9-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate required CLI arguments before indexing sys.argv.

Invoking this diagnostic without both paths raises IndexError instead of stating the expected invocation.

Proposed fix
+if len(sys.argv) != 3:
+    raise SystemExit(
+        f"usage: {sys.argv[0]} INPUT_TRACE.csv OUTPUT_REPORT.txt"
+    )
+
 rows=load(sys.argv[1])

As per path instructions, developer tooling should prioritize correctness and clear error messages.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

if len(sys.argv) != 3:
    raise SystemExit(
        f"usage: {sys.argv[0]} INPUT_TRACE.csv OUTPUT_REPORT.txt"
    )

rows=load(sys.argv[1])
out=[]
out.append("rows=%d"%len(rows))
# The 4 exact-timed reads (ReadFrom2002WithExactTiming) are ~29550 cyc apart and
# each ends on a slightly later dot. Find reads whose FORWARD gap to next is in
# 29000..30500 — those are the isolated measurement reads (surrounded by a big
# clockslide). Also catch the LAST one (no big forward gap) by also taking reads
# whose BACKWARD gap is in that range.
iso=set()
for i in range(len(rows)):
    fg = rows[i+1][0]-rows[i][0] if i+1<len(rows) else 0
    bg = rows[i][0]-rows[i-1][0] if i>0 else 0
    if 29000<=fg<=30500 or 29000<=bg<=30500:
        iso.add(i)
out.append("isolated (exact-timed) reads: %d"%len(iso))
for i in sorted(iso):
    r=rows[i]
    out.append("  cyc=%d mc=%d sl=%d dot=%d val=0x%02X masked0xE0=0x%02X (V%d S%d O%d)"%(
        r[0],r[1],r[2],r[3],r[4],r[4]&0xE0,(r[4]>>7)&1,(r[4]>>6)&1,(r[4]>>5)&1))
open(sys.argv[2],'w').write("\n".join(out)+"\n")
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 27-27: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(sys.argv[2],'w')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 11-11: Use format specifiers instead of percent format

(UP031)


[warning] 23-23: Use format specifiers instead of percent format

(UP031)


[warning] 26-26: Use format specifiers instead of percent format

(UP031)


[warning] 28-28: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/locate_first_trace_divergence.py` around lines 9 - 28, Validate
that the diagnostic receives both required path arguments before
`load(sys.argv[1])` accesses `sys.argv`. When either argument is missing,
terminate with a clear usage message describing the expected invocation;
otherwise preserve the existing row-processing and output behavior.

Source: Path instructions

scripts/diag/ppu2002_read_value_histogram.py (2)

3-3: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Other (CWE-377): Insecure Temporary File

Avoid predictable shared temporary paths.

Lines 3 and 37 trust /tmp/RustyNES and overwrite a predictable output name. A local attacker can pre-create that directory and symlink an_out.txt, causing this tool to overwrite a file writable by its user. Accept explicit paths or create an owned temporary directory/file safely.

Also applies to: 37-37

🧰 Tools
🪛 Ruff (0.15.21)

[error] 3-3: Probable insecure usage of temporary file or directory: "/tmp/RustyNES/s2002.csv"

(S108)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` at line 3, Update the file
handling around the histogram input and output paths to avoid the predictable
/tmp/RustyNES location and fixed filenames. Accept explicit paths or create a
securely owned temporary directory/files using exclusive creation, and ensure
both the open at the top level and the output handling near the script’s line 37
use the safe path strategy.

Source: Linters/SAST tools


30-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce the diagnostic window’s timing constraints.

Line 34 matches only the VSO mask sequence; it does not require a shared prerender scanline or consecutive dots despite Lines 27-33 describing both. Unrelated sequential trace rows can therefore be reported as a four-read transition. Check the scanline and expected dot progression before appending a match.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 36-36: Do not hardcode temporary file or directory names
Context: '/tmp/RustyNES/an_out.txt'
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.15.21)

[error] 35-35: Multiple statements on one line (semicolon)

(E702)


[warning] 36-36: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 30 - 36, Update
the diagnostic window logic around the loop and its out.append call to require
all four rows share the prerender scanline identified by the surrounding timing
checks and have consecutive dots following the expected progression. Only append
a histogram match after validating these timing constraints in addition to the
existing mask sequence checks.
scripts/diag/ppu2004_diff_vs_answerkey.py (1)

42-48: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject incomplete captures before reporting comparison results.

All three diagnostics omit absent dot indices, so a truncated trace can report zero mismatches rather than an invalid capture.

  • scripts/diag/ppu2004_diff_vs_answerkey.py#L42-L48: require all 341 expected dots and treat missing indices as an error or mismatches.
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py#L42-L48: apply the same complete-capture validation.
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py#L20-L23: validate complete Mesen and RustyNES dot maps before either comparison.
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 44-44: Use dot2val.get(d) instead of dot2val.get(d, None)

Replace dot2val.get(d, None) with dot2val.get(d)

(SIM910)


[error] 45-45: Multiple statements on one line (colon)

(E701)

📍 Affects 3 files
  • scripts/diag/ppu2004_diff_vs_answerkey.py#L42-L48 (this comment)
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py#L42-L48
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py#L20-L23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2004_diff_vs_answerkey.py` around lines 42 - 48, Validate
that all 341 expected dot indices are present before reporting comparison
results: update scripts/diag/ppu2004_diff_vs_answerkey.py lines 42-48 and
scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py lines 42-48 to treat
missing indices as capture errors or mismatches, and update
scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py lines 20-23 to validate
both dot maps before comparison.
scripts/diag/ppu2007_offset_greedy_search.py (2)

7-9: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail clearly when the diagnostic command does not produce a dump.

A nonzero scan_dma_abort exit or missing STRESS2007= line reaches Line 8 and raises an opaque IndexError; stderr is discarded. Require a successful subprocess result and explicitly reject absent or malformed markers. As per path instructions, “Prioritize correctness and clear error messages over style.”

🧰 Tools
🪛 Ruff (0.15.21)

[error] 7-7: subprocess call: check for execution of untrusted input

(S603)


[warning] 7-7: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


[warning] 8-8: Prefer next(l for l in out.splitlines() if 'STRESS2007=' in l) over single element slice

Replace with next(l for l in out.splitlines() if 'STRESS2007=' in l)

(RUF015)


[error] 8-8: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_offset_greedy_search.py` around lines 7 - 9, Update the
diagnostic command handling around subprocess.run and the STRESS2007 parsing to
require a successful scan_dma_abort exit, preserve stderr for failure reporting,
and raise a clear error when the command fails, the marker is absent, or its
value is not valid hexadecimal. Replace the unchecked list indexing in the
parsing flow while preserving the existing data conversion for valid output.

Source: Path instructions


11-16: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Score both permitted one-dot rotations.

The diagnostic contract allows ±1 alignment, but Line 13 tries only the unrotated dump and [d[-1]] + d[:-1]. Include the opposite rotation (d[1:] + d[:1]) or the greedy search can select offsets from an artificially reduced score.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 13-13: Consider [d[-1], *d[:-1]] instead of concatenation

Replace with [d[-1], *d[:-1]]

(RUF005)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_offset_greedy_search.py` around lines 11 - 16, Update
the rotation candidates in the greedy score calculation to include both
permitted one-dot rotations: retain the unrotated dump and existing right
rotation, and add the opposite left rotation using d[1:] + d[:1]. Ensure best
scoring evaluates all three candidates.
scripts/diag/ppu2007_stress_per_index_evaluator.py (1)

22-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate CLI arguments before reading the dump.

Invoking the documented tool without <dump-file> raises IndexError instead of printing usage and exiting with a command-line error. Validate sys.argv before Line 22. As per path instructions, “Prioritize correctness and clear error messages over style.”

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 22-22: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py` at line 22, Validate the
command-line arguments before the file read in the script’s top-level flow,
ensuring a missing dump-file argument prints clear usage information and exits
with a command-line error instead of allowing sys.argv[1] to raise IndexError.
Keep the existing open/read behavior unchanged when the required argument is
present.

Source: Path instructions

scripts/mapper-promotion/batch2.py (1)

79-79: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use a real output path for generated test bodies. Both generators write to a session-specific /tmp/claude-1000/... directory that is absent outside the original capture session, so they terminate with FileNotFoundError after copying or scanning ROMs.

  • scripts/mapper-promotion/batch2.py#L79-L79: accept an output path argument or write to a deliberate repository-local output path.
  • scripts/mapper-promotion/gen_promotion.py#L57-L57: use the same configurable output-path contract.
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 79-79: Use a context manager for opening files

(SIM115)


[error] 79-79: Probable insecure usage of temporary file or directory: "/tmp/claude-1000/-home-parobek-Code-OSS-Public-Projects-RustyNES/6112789e-19c9-4d7b-9638-b241cdbc833d/scratchpad/promo_tests_2.rs"

(S108)

📍 Affects 2 files
  • scripts/mapper-promotion/batch2.py#L79-L79 (this comment)
  • scripts/mapper-promotion/gen_promotion.py#L57-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/batch2.py` at line 79, Replace the hard-coded
session-specific output path used by batch2.py’s generator with a configurable
output-path argument or deliberate repository-local default, and apply the same
output-path contract to gen_promotion.py. Ensure both generators write generated
test bodies to an existing, valid destination while preserving their current
output content.

Source: Linters/SAST tools

scripts/mapper-promotion/enumerate_staged.py (1)

60-65: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not promote a ROM after mapper-header verification fails. Both scripts replace a failed match with the first archive entry, allowing a different mapper to be reported or generated as extended_m{mid}. That invalidates the proposed tier evidence.

  • scripts/mapper-promotion/enumerate_staged.py#L60-L65: leave mismatches out of rows and promoted_ids; report them as failures.
  • scripts/mapper-promotion/gen_promotion.py#L36-L43: fail generation for an unmatched mapper instead of emitting a fallback ROM path.
📍 Affects 2 files
  • scripts/mapper-promotion/enumerate_staged.py#L60-L65 (this comment)
  • scripts/mapper-promotion/gen_promotion.py#L36-L43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/enumerate_staged.py` around lines 60 - 65, Prevent
failed mapper-header matches from being promoted: in
scripts/mapper-promotion/enumerate_staged.py lines 60-65, keep the mismatch in
failure reporting but omit it from rows and promoted_ids instead of selecting
roms[0]; in scripts/mapper-promotion/gen_promotion.py lines 36-43, fail
generation for an unmatched mapper rather than emitting a fallback ROM path or
extended_m{mid} output.
scripts/mapper-promotion/scan.py (1)

20-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Process every .nes member in ZIP archives.

Using names[0] makes mapper classification depend on archive member order. A multi-ROM archive can therefore be assigned to the wrong target or hide a supported mapper. Iterate all .nes members and retain the member path in the reported candidate, or reject multi-ROM archives explicitly.

As per path instructions, developer tooling should prioritize correctness and clear error messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/scan.py` around lines 20 - 28, Update get_header so
ZIP archives process every .nes member instead of selecting names[0], allowing
mapper classification to inspect all ROMs and avoid archive-order dependence.
Preserve each member’s archive path when constructing the reported candidate, or
explicitly reject archives containing multiple .nes members with a clear error;
keep the existing single-file behavior unchanged.

Source: Path instructions

scripts/pr-review/list_unresolved_threads.py (1)

6-7: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="scripts/pr-review/list_unresolved_threads.py"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== file excerpt =="
nl -ba "$file" | sed -n '1,220p'

Repository: doublegate/RustyNES

Length of output: 2111


Injection (CWE-150)

Reachability: External

Escape PR-sourced fields before printing The path, author login, and body come from the pull request and are written to the terminal with only newline replacement. Encode or visibly escape ESC/C1 control characters before printing any externally sourced field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_unresolved_threads.py` around lines 6 - 7, Update the
output statements in the unresolved-thread listing to sanitize every
pull-request-sourced field before printing, including the thread path, author
login, and truncated body. Apply a visible escaping or encoding step that covers
ESC and C1 control characters while preserving the existing newline flattening
and output structure.
tests/roms/assorted/README.md (1)

34-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the CPU timing description with its documented source.

tests/roms/LICENSES.md identifies this fixture as cpu_timing_test6/cpu_timing_test.nes, so calling it “more comprehensive than cpu_timing_test6” is internally contradictory. Describe the behavior without comparing it to the suite that contains the file.

Proposed wording
-- **`cpu_timing_test.nes`** — End-to-end CPU per-instruction cycle
-  count (more comprehensive than `cpu_timing_test6`).
+- **`cpu_timing_test.nes`** — End-to-end CPU per-instruction cycle
+  count from the `cpu_timing_test6` suite.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

- **`cpu_timing_test.nes`** — End-to-end CPU per-instruction cycle
  count (more comprehensive than `cpu_timing_test6`).
 **`cpu_timing_test.nes`** — End-to-end CPU per-instruction cycle
  count from the `cpu_timing_test6` suite.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/roms/assorted/README.md` around lines 34 - 35, Update the
cpu_timing_test.nes entry in the README to describe its end-to-end
per-instruction CPU cycle-counting behavior without claiming it is more
comprehensive than cpu_timing_test6. Keep the description consistent with the
fixture’s documented source path in LICENSES.md.

Source: Path instructions

to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md (1)

358-360: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the mapper-module path migration in both documentation references.

The release updates the naming convention but leaves old paths in two touched documentation sections.

  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L358-L360: replace the remaining crates/rustynes-mappers/src/sprintN.rs reference at Line 364, or mark it explicitly as historical.
  • to-dos/ROADMAP.md#L334-L340: replace crates/rustynes-mappers/src/mmc2_mmc4.rs with crates/rustynes-mappers/src/m010_mmc4.rs.
📍 Affects 2 files
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L358-L360 (this comment)
  • to-dos/ROADMAP.md#L334-L340
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md` around lines 358 - 360, Complete
the mapper-module path migration in both documentation references: in
to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md lines 358-360, update the remaining
crates/rustynes-mappers/src/sprintN.rs reference or explicitly mark it
historical; in to-dos/ROADMAP.md lines 334-340, replace
crates/rustynes-mappers/src/mmc2_mmc4.rs with
crates/rustynes-mappers/src/m010_mmc4.rs.

Source: Path instructions

crates/rustynes-mappers/src/m033_taito_tc0190.rs (1)

106-117: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fixed-tail bank math can underflow for a single-bank PRG image.

With an 8 KiB PRG-ROM bank_count == 1, so bank_count - 2 (slot 2, $C000) underflows usize before the % bank_count runs — a debug panic on a cpu_read, on the untrusted-ROM path. Mapper 33 images are realistically larger, but the module contract is no panic on register access.

Proposed guard
-            2 => bank_count - 2, // fixed second-last
-            _ => bank_count - 1, // fixed last
+            2 => bank_count.saturating_sub(2), // fixed second-last
+            _ => bank_count.saturating_sub(1), // fixed last
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn read_prg(&self, addr: u16) -> u8 {
        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
        let bank = match slot {
            0 => self.prg_bank[0] as usize,
            1 => self.prg_bank[1] as usize,
            2 => bank_count.saturating_sub(2), // fixed second-last
            _ => bank_count.saturating_sub(1), // fixed last
        } % bank_count;
        let off = (addr as usize) & (PRG_BANK_8K - 1);
        self.prg_rom[bank * PRG_BANK_8K + off]
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs` around lines 106 - 117,
Update read_prg so fixed-tail bank selection for slots 2 and 3 cannot underflow
when bank_count is less than two. Preserve the normal second-last/last mapping
for larger images while ensuring single-bank PRG reads remain in range and never
panic during register access.
crates/rustynes-mappers/src/m041_caltron41.rs (1)

37-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove mechanical-split leftovers: stray comment blocks, doubled #[cfg(test)], and joined //! doc lines.

The sprint→board module split left artifacts that document boards which now live in their own mNNN_* files (misleading a reader into thinking that mapper's code is in this file), duplicated the #[cfg(test)] attribute, and merged the //! paragraph break onto the end of a prose line.

  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43: delete the stray "Mapper 38 — Bit Corp UNL-PCI556" block, and also the "Mapper 232 — Camerica Quattro / BF9096" block at L207-L216 (both boards live elsewhere).
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96: delete the stray "Mmc3Clone" block and the "Mapper 50" block at L300-L310; collapse the doubled #[cfg(test)] at L312-L313 to one; and split the joined //! at L12 (... in fds.rs.//!) onto its own line.
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70: delete the stray "Mmc3Clone" block and the "DiscreteMapper" block at L240-L243; collapse the doubled #[cfg(test)] at L245-L246; and split the joined //! at L9 (... gets wrong.//!).
  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73: delete the stray "Mapper 28 — Action 53" block and the "Mapper 174 — NTDEC" block at L217-L225; collapse the doubled #[cfg(test)] at L227-L228; and split the joined //! at L8 (... m095_namcot3425.rs.//!).
📍 Affects 4 files
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43 (this comment)
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70
  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m041_caltron41.rs` around lines 37 - 43, Remove
the stale board comment blocks, duplicate test attributes, and joined
documentation lines from the split mapper modules. In
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43, remove the Mapper 38
block and also remove the Mapper 232 block at lines 207-216; in
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs lines 80-96, remove the
Mmc3Clone and Mapper 50 blocks, reduce the duplicated #[cfg(test)] at lines
312-313 to one, and put the joined //! text at line 12 on its own line; apply
the corresponding removals, attribute collapse, and //! line split in
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs lines 54-70 and
crates/rustynes-mappers/src/m076_namcot3446.rs lines 55-73 as specified.
crates/rustynes-mappers/src/m048_taito_tc0690.rs (1)

132-143: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

bank_count - 2 underflows for a single-bank (8 KiB) PRG.

new accepts any non-zero multiple of 8 KiB, so bank_count can be 1. A $C000-$FFFF read then hits slot 2 → 1 - 2, which panics in debug builds and wraps in release. Sibling m076_namcot3446.rs (Line 138-140) already documents this exact single-8 KiB-bank case and guards it with saturating_sub; mirror that here.

Proposed fix (matches m076 convention)
         let bank = match slot {
             0 => self.prg_bank[0] as usize,
             1 => self.prg_bank[1] as usize,
-            2 => bank_count - 2,
-            _ => bank_count - 1,
+            2 => bank_count.saturating_sub(2),
+            _ => bank_count.saturating_sub(1),
         } % bank_count;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn read_prg(&self, addr: u16) -> u8 {
        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
        let bank = match slot {
            0 => self.prg_bank[0] as usize,
            1 => self.prg_bank[1] as usize,
            2 => bank_count.saturating_sub(2),
            _ => bank_count.saturating_sub(1),
        } % bank_count;
        let off = (addr as usize) & (PRG_BANK_8K - 1);
        self.prg_rom[bank * PRG_BANK_8K + off]
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m048_taito_tc0690.rs` around lines 132 - 143,
Update read_prg’s fixed-bank mapping for slot 2 to use saturating subtraction
when deriving bank_count - 2, matching the m076_namcot3446 convention. Preserve
the existing slot mapping and modulo behavior while ensuring single-bank PRG
reads cannot underflow.
crates/rustynes-ppu/src/snapshot.rs (1)

491-491: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the const-bound range-pattern style already compiles/is used elsewhere in the workspace
# (precedent: rustynes-apu's snapshot.rs), and check whether any other chip snapshot readers
# hardcode a version-count literal that could drift from their VERSION const.
rg -nP '!matches!\(version,\s*1\.\.=' crates/rustynes-*/src/snapshot.rs

Repository: doublegate/RustyNES

Length of output: 2140


Tie the PPU snapshot version check to PPU_SNAPSHOT_VERSION.
matches!(version, 1..=8) duplicates the version constant as a literal. Use 1..=PPU_SNAPSHOT_VERSION so the acceptance bound stays in one place when the snapshot schema bumps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-ppu/src/snapshot.rs` at line 491, Update the snapshot version
validation around the version check to use the `PPU_SNAPSHOT_VERSION` constant
as the upper bound instead of the duplicated literal `8`, preserving acceptance
of versions from 1 through the current schema version.

@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

The previous run reported all four custom pre-merge checks as inconclusive with "Repository clone failed, so this custom check could not run with code access" (Docs-As-Spec Sync, Changelog Entry For User-Visible Changes, No Unwrap/Expect/Panic On Untrusted Input, Safety Comment On New Unsafe Blocks). Those are the checks that matter most for this repository, and zero line comments were posted, so retrying per the resolution guidance.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

@doublegate Understood. I’m rerunning the full review so the repository-dependent pre-merge checks are evaluated with code access, with priority on documentation/changelog alignment and Rust panic/unsafe boundaries rather than CI-covered style concerns.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

@doublegate Understood. Rerunning the complete review for #325, prioritizing the four previously inconclusive repository-dependent checks and correctness in rustynes-* chip crates over CI-covered style findings.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 55

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/STATUS.md (1)

1462-1482: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove duplicated mapper-lineage phrases.

Line 1464 repeats NTDEC/TXC/BMC on the following line, and lines 1478-1479 repeat reusable-ASIC. This makes the authoritative coverage history malformed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/STATUS.md` around lines 1462 - 1482, Remove the duplicated NTDEC/TXC/BMC
phrase in the mapper coverage summary and the repeated reusable-ASIC phrase in
the v1.7.0 mapper count entry. Preserve the surrounding mapper lineage and
version/count information unchanged.
crates/rustynes-core/src/input_device.rs (1)

267-272: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale doc comment: this "future refinement" is now implemented in the same file.

The sample_light docstring still describes full per-dot temporal integration against the beam position as a "documented future refinement," but light_at_scanline/read_at_scanline (added immediately below in this same diff) implement exactly that. Left as-is, this misleads future readers into thinking the refinement doesn't exist yet.

📝 Suggested doc fix
-    /// The temporal light-sense window (the ~19-26-scanline photodiode hold) is
-    /// finer than the per-frame sample resolution used here; the supported
-    /// light-gun titles re-poll every frame, so frame-granular sampling of the
-    /// presented framebuffer is sufficient. A full per-dot temporal integration
-    /// against the beam position is a documented future refinement — see
-    /// `docs/frontend.md`.
+    /// The temporal light-sense window (the ~19-26-scanline photodiode hold) is
+    /// finer than the per-frame sample resolution used here; the supported
+    /// light-gun titles re-poll every frame, so frame-granular sampling of the
+    /// presented framebuffer is sufficient for them. A full per-dot temporal
+    /// integration against the beam position is available, opt-in, via
+    /// [`Self::light_at_scanline`] / [`Self::read_at_scanline`] (A3, v2.2.3) —
+    /// see `docs/frontend.md`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/input_device.rs` around lines 267 - 272, Update the
`sample_light` documentation to remove the claim that per-dot temporal
integration is a future refinement, and describe the implemented
`light_at_scanline`/`read_at_scanline` beam-position sampling instead. Keep the
existing explanation of frame-granular behavior only if it remains accurate for
the current implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/release-notes/v2.2.3.md:
- Line 77: Clarify the release notes at .github/release-notes/v2.2.3.md:77 and
CHANGELOG.md:213-219 by separating netplay format compatibility from rollback
snapshot compatibility: state that .rnm and netplay wire formats remain
unaffected, while rollback snapshots use the updated save-state schema.
- Around line 105-109: Correct the elapsed-time statement describing the stale
snapshots in both .github/release-notes/v2.2.3.md lines 105-109 and CHANGELOG.md
lines 81-87: change “28 days earlier” to “40 days earlier,” or update the
referenced source date so the calculated duration is accurate.
- Line 19: Update the release note to state that the [emulation] config key uses
#[serde(default = "default_fast_dotloop")] and therefore defaults fast dotloop
to enabled for existing configurations; do not describe it as bare
#[serde(default)], since that would imply bool::default() disables the
optimization.
- Around line 133-134: Update both cargo clippy command entries in
.github/release-notes/v2.2.3.md (lines 133-134) and CHANGELOG.md (lines 289-291)
to place -D warnings after the -- separator, preserving the existing workspace,
package, feature, and target options.

In @.github/workflows/ios.yml:
- Around line 63-77: Update the Xcode bundle selection pipeline in the xc
assignment to sort versioned Xcode_26.*.app directories by their numeric minor
suffix rather than lexical path order, ensuring Xcode_26.10.app is selected over
Xcode_26.9.app while preserving support for the canonical Xcode_26.app bundle.

In `@AGENTS.md`:
- Line 188: Update AGENTS.md at line 188 by changing the v2.2.2 history wording
from “this release” to “that release” and qualify the claim that no build path
uses nightly. Apply the corresponding qualification to the “no nightly on any
build path” statement in README.md lines 806-809, preserving the documented
exceptions.

In `@CHANGELOG.md`:
- Around line 37-40: Use a single authoritative test total across both release
documents: reconcile the suite result, then update the differing entry in
CHANGELOG.md (lines 37-40) or .github/release-notes/v2.2.3.md (line 7) so both
report the same passed/failed counts.

In `@crates/rustynes-core/src/bus.rs`:
- Around line 1631-1662: Remove the copied overlay-device description and its
invalid # Panics clause from set_zapper_temporal_light; retain only the A3
beam-relative Zapper light model documentation that accurately describes the on
parameter and behavior.

In `@crates/rustynes-core/src/input_device.rs`:
- Around line 1761-1772: Update the fb_with_target test helper to prevent
underflow when x or y is zero by using saturating subtraction for the lower
bounds of both pixel ranges. Preserve the existing inclusive three-pixel
neighborhood and upper-bound behavior.
- Around line 1839-1846: Update zapper_temporal_off_screen_never_sees_light to
keep the aim’s y coordinate on-screen while placing only x off-screen, then
retain scanlines at or beyond that y so light_at_scanline reaches
aperture_is_bright’s off-screen boundary check rather than returning through the
scanline < y gate.

In `@crates/rustynes-mappers/src/bmc_simple.rs`:
- Around line 376-385: Make the M286 `$A000` handling in the `SimpleBoard::M286`
branch safe for untrusted `self.dip` values by converting or masking the dip
index before adding the shift offset, ensuring the resulting shift is valid for
`u16`. Preserve the existing enable-bit check and PRG bank update behavior for
valid dip values, and avoid any panic-prone operations on restored state data.

In `@crates/rustynes-mappers/src/m005_mmc5.rs`:
- Around line 1479-1481: Update the mix arithmetic at the return expression to
promote pulse_mix, pcm_mix, and MMC5_MIX_BIAS to i32 before addition and
subtraction, ensuring overflow cannot occur in the intermediate i16 calculation
while preserving the existing scaled mix result.

In `@crates/rustynes-mappers/src/m010_mmc4.rs`:
- Around line 242-286: Update save_state and load_state in
crates/rustynes-mappers/src/m010_mmc4.rs:242-286,
crates/rustynes-mappers/src/m021_vrc4.rs:401-458, and
crates/rustynes-mappers/src/m022_vrc2.rs:311-347 to append prg_ram after the
existing snapshot data and restore it during loading. Adjust expected lengths
and payload offsets accordingly, following the established serialization pattern
in the MMC5 and Jaleco SS88006 mappers.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs`:
- Around line 106-117: Guard the fixed-tail bank calculations in read_prg for
crates/rustynes-mappers/src/m033_taito_tc0190.rs lines 106-117 and
crates/rustynes-mappers/src/m048_taito_tc0690.rs lines 132-143 by using
saturating subtraction for bank_count - 2 and bank_count - 1, or reject PRG
images with fewer than two banks in each constructor; preserve valid bank
selection for multi-bank images.

In `@crates/rustynes-mappers/src/m034_bnrom_nina001.rs`:
- Around line 190-230: Update save_state and load_state to persist self.chr when
chr_is_ram is true, matching the conditional CHR-RAM serialization used by
sibling mapper modules. Append and restore the CHR data consistently while
preserving the existing state layout for variants without CHR-RAM, and include
its length in validation and capacity calculations.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs`:
- Line 6: Split the run-together module documentation into separate lines at
crates/rustynes-mappers/src/m036_txc_policeman.rs lines 6-6,
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs lines 12-12, and
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs lines 9-9: keep each sentence
ending at its period, then add a separate blank //! line so rustdoc does not
render the marker literally.
- Around line 53-65: Remove or retarget the stale section banners so each header
describes the implementation immediately following it: update the Mapper 15
banner above Txc36 in crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65,
and remove the unrelated Mapper 39 banner at `#L189-L196`; remove the unrelated
Mapper 79 banner in crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167;
remove or retarget the Mmc3Clone and Mapper 50 banners in
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96 and `#L300-L310`;
and remove or retarget the Mmc3Clone and DiscreteMapper banners in
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70 and `#L240-L243`.

In `@crates/rustynes-mappers/src/m039_subor39.rs`:
- Around line 40-52: Remove the unrelated standalone mapper documentation
blocks: in crates/rustynes-mappers/src/m039_subor39.rs lines 40-52 remove the
“Mapper 15 — K-1029” block and lines 192-203 remove the “Mapper 61” block; in
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43 remove the “Mapper 38
— Bit Corp” block and lines 207-216 remove the “Mapper 232 — Camerica” block.
Keep documentation describing each file’s actual mapper implementation.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs`:
- Around line 227-228: Remove the duplicate #[cfg(test)] attribute from the
tests module in crates/rustynes-mappers/src/m076_namcot3446.rs lines 227-228,
crates/rustynes-mappers/src/m077_irem_napoleon.rs lines 222-223,
crates/rustynes-mappers/src/m079_ave_nina03_06.rs lines 226-227, and
crates/rustynes-mappers/src/m095_namcot3425.rs lines 251-252, leaving one
attribute per module.
- Around line 55-73: Remove the orphaned foreign-mapper banner blocks and
restore the separated blank doc lines in
crates/rustynes-mappers/src/m076_namcot3446.rs (lines 55-73, plus the Mapper 174
block and joined line at line 8),
crates/rustynes-mappers/src/m077_irem_napoleon.rs (lines 47-59, Mapper 96 block,
and joined line at line 7), crates/rustynes-mappers/src/m079_ave_nina03_06.rs
(lines 53-59, Mapper 113 block, and joined line at line 11), and
crates/rustynes-mappers/src/m095_namcot3425.rs (lines 53-71, Mapper 112 block,
and joined line at line 7). In crates/rustynes-mappers/src/m096_bandai96.rs
(lines 44-56), also remove the Mapper 97 block; make no functional mapper
changes.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs`:
- Around line 10-13: Correct the CHR bank mapping description in the module
documentation to state that the bank is computed as L shifted by 0 OR H shifted
by 1, matching the adjacent bit formula, the implementation, and the
bit_swapped_chr_select test. Change only the contradictory prose and preserve
the existing formula and code.

In `@crates/rustynes-mappers/src/m093_sunsoft3r.rs`:
- Around line 106-112: Update cpu_write to apply bus-conflict masking before
decoding the Sunsoft-3R register: AND the written value with the PRG-ROM byte
read at the same address, reusing the established mapper-89/Sunsoft-2 behavior.
Decode the masked value for prg_bank and chr_ram_enabled while preserving the
existing 0x8000..=0xFFFF address guard.

In `@crates/rustynes-mappers/src/m094_un1rom.rs`:
- Around line 177-185: The split mapper files contain leftover documentation and
duplicated test attributes. In
crates/rustynes-mappers/src/m094_un1rom.rs:177-185, remove the dead Mapper 101
comment and retain only one #[cfg(test)] before mod tests; in
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65, remove or relocate the
untracked Mapper 15 comment; and in
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:221-236, remove the superseded
Mapper 132 comment and duplicate #[cfg(test)], keeping one. Sweep other newly
split mapper files for the same residue.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs`:
- Around line 150-155: Update the `$C000-$DFFF` branch in the `match addr` logic
to compute the bank index with saturating subtraction, ensuring `last == 0`
remains valid instead of panicking. Leave the other PRG window mappings
unchanged.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs`:
- Around line 40-47: Remove the orphaned neighbouring-board banner comments,
leaving each module’s implementation unchanged: in
crates/rustynes-mappers/src/m107_magic_dragon107.rs at lines 40-47 and 168-175
remove the Mapper 31 and Mapper 143 blocks; in
crates/rustynes-mappers/src/m132_txc_22211.rs at lines 58-70 and 268-275 remove
the Mapper 15 and Mapper 133 blocks; in
crates/rustynes-mappers/src/m177_hengedianzi.rs at lines 52-59 and 182-188
remove the Mapper 31 and Mapper 179 blocks; in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 47-53 and 195-207
remove the Mapper 38 and Mapper 250 blocks; in
crates/rustynes-mappers/src/m250_nitra250.rs at lines 42-58 remove the Mapper 40
block; in crates/rustynes-mappers/src/m268_bmc_coolboy.rs at lines 445-451
remove the Sachen9602 block; in crates/rustynes-mappers/src/m176_bmc_fk23c.rs at
lines 541-549 remove the Coolboy block; and in
crates/rustynes-mappers/src/m513_sachen_9602.rs at lines 341-344 remove the
TxcChip block.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs`:
- Around line 48-54: Remove the stray Mapper 38 and Mapper 86 section-header
comment blocks from this file, leaving their actual implementation files
unchanged. In the test module declaration, remove the duplicated #[cfg(test)]
attribute so only one remains.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs`:
- Around line 206-209: Update the 0x8000..=0xFFFF PRG read branch to wrap the
computed ROM offset using self.prg_rom.len() directly, rather than count *
PRG_BANK_32K. Preserve the existing address masking while ensuring valid
indexing for all PRG lengths accepted by check_prg.

In `@crates/rustynes-mappers/src/m156_daou156.rs`:
- Around line 42-58: Remove the stale mapper header blocks from all affected
files: delete the Mapper 40 and Mapper 162 blocks in
crates/rustynes-mappers/src/m156_daou156.rs#L42-L58 and its sibling block at
L229-L244; delete the Mapper 31 and Mapper 58 blocks in
crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60 and L183-L193; delete
the Mapper 147 and Mapper 185 blocks in
crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58 and L215-L229; and
delete the Mapper 147 and Mapper 200 blocks in
crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62 and L234-L245, leaving only
comments describing each file’s implemented mapper.

In `@crates/rustynes-mappers/src/m179_hengedianzi.rs`:
- Line 6: Update the module-level documentation line in m179_hengedianzi.rs so
the sentence ends with “ordinary ROM write.” and the paragraph-breaking `//!`
marker appears on its own separate line, preventing it from rendering as literal
prose.
- Around line 195-197: Remove the redundant duplicate #[cfg(test)] attribute
immediately before the tests module, leaving a single conditional-compilation
attribute on mod tests.

In `@crates/rustynes-mappers/src/m210_namco175.rs`:
- Line 296: Update the save-state serialization in the mapper’s state-save
method to replace the Mirroring enum cast with the existing or added explicit
mirroring_to_byte mapping, paired with byte_to_mirroring in load_state. Preserve
the fixed 0..5 save-format mapping independently of Mirroring declaration order,
following the sibling mapper implementations.

In `@crates/rustynes-mappers/src/m232_camerica_bf9096.rs`:
- Around line 38-44: Remove or realign orphaned mapper section headers so each
documents the correct implementation. In
crates/rustynes-mappers/src/m232_camerica_bf9096.rs:38-44 and 191-200, remove
the misplaced Mapper 38 and Mapper 240 headers; do likewise for
crates/rustynes-mappers/src/m240_cne_multicart.rs:47-53 and 177-183,
m241_bxrom241.rs:35-41, m246_fong_shen_bang246.rs:41-59, and
mmc3_clones.rs:767-780. Realign or remove the headers around Multicart61 and
Mapper 72 in multicart_discrete.rs:295-301 and 595-606; Ntdec63 and the other
listed NTDEC implementations in ntdec.rs:60-78, 246-260, 411-422, and 727-741;
Sachen8259M137 and its related implementation in sachen_8259.rs:58-74 and
248-261; SachenTca01M143 in sachen_discrete.rs:1273-1282; and Waixing242 and
related implementations in waixing.rs:63-81, 270-284, and 647-656. Ensure every
remaining header matches the mapper code immediately following it.
- Around line 57-62: Update the constructor documentation for the mapper 232
implementation near new() so it accurately states that an 8 KiB supplied CHR-ROM
is accepted and used read-only, rather than claiming all CHR-ROM input is
rejected. Keep the existing PRG validation and CHR size requirements documented
unchanged.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs`:
- Line 5: Fix the module-level documentation comment at the top of the file by
separating the trailing `//!` onto its own line, restoring the intended
paragraph break so rustdoc does not render it inline after “using.”
- Around line 185-186: Remove the duplicated #[cfg(test)] attribute in the test
module declaration, preserving a single #[cfg(test)] and restoring the sibling
module’s #[allow(clippy::cast_possible_truncation)] attribute on the second line
if it was clobbered.

In `@crates/rustynes-mappers/src/m250_nitra250.rs`:
- Around line 127-137: Update the prg_bank_for method’s fixed-bank match arm to
use saturating subtraction when deriving the bank before last, preventing
underflow when last is zero while preserving the existing bank selection for
larger PRG images.

In `@crates/rustynes-mappers/src/multicart_discrete.rs`:
- Around line 12-20: Update the module headers to describe the implementations
as using best-effort verification methodology while stating that tier
classification varies by mapper id, and remove the blanket
MapperTier::BestEffort reference where it is inaccurate. Apply this to the
headers in crates/rustynes-mappers/src/multicart_discrete.rs (Curated ids
15/61/62/200/201/202/203/212/213/214/225/226/227/229/231/233/234; BestEffort ids
104/174/290/299/301), crates/rustynes-mappers/src/m246_fong_shen_bang246.rs (246
Curated), crates/rustynes-mappers/src/mmc3_clones.rs (Curated
44/49/52/115/134/189/205/245; BestEffort 238/348/366),
crates/rustynes-mappers/src/ntdec.rs (Curated 63/40/112/193/221; BestEffort
174/81/299), crates/rustynes-mappers/src/sachen_8259.rs (all Curated),
crates/rustynes-mappers/src/sachen_discrete.rs (all Curated), and
crates/rustynes-mappers/src/waixing.rs (all Curated), while preserving accurate
references to tier.rs.

In `@crates/rustynes-test-harness/tests/fast_dotloop_diff.rs`:
- Around line 246-316: Correct vblank_io_torture_rom and its accompanying
documentation so the test no longer claims that $2006/$2007 exercise idle-line
countdowns during vblank. Either retime the ROM to place those accesses in the
last visible scanline’s fetch window and spill into idle time, or narrow the
comments to state that only $2001 reliably covers the idle-line fast-path
fallback.

In `@crates/rustynes-test-harness/tests/holy_mapperel.rs`:
- Around line 76-81: Update the residual section heading and introductory text
around the M1_* and M69_* cases to reflect the corrected detail=0000 results,
removing the claim that both families produce a nonzero WRAM nibble. Preserve
the distinction between MMC1’s always-enabled WRAM behavior and FME-7’s modeled
RAM-enable/open-bus behavior.

In `@docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md`:
- Around line 156-163: Update the “Follow-up” section of the ADR to state that
the struct-versus-serializer comparison is already implemented as a standing
test in snapshot_schema_audit.rs, rather than presenting it as future work.
Preserve the existing description of the check and its allowlist behavior while
removing or revising the speculative “worth running” and “worth considering”
wording.

In `@docs/apu-2a03.md`:
- Around line 345-353: Reconcile the documented Mapper::mix_audio contract with
the i32 implementation: update docs/apu-2a03.md lines 345-353 to show an i32
return type, and update docs/expansion-audio.md line 19 to use the i32 hook
signature and describe Sunsoft 5B absolute level as calibrated/asserted rather
than deferred.

In `@docs/mappers.md`:
- Around line 260-262: Update the per-board module references in the
documentation list: replace ave_nina.rs with m079_ave_nina03_06.rs and cne240.rs
with m240_cne_multicart.rs, leaving the other module names and tier description
unchanged.

In `@scripts/agy-review.sh`:
- Around line 187-194: The fetch path exposes GH_TOKEN in the git process
arguments. Update the token-authenticated branch around fetch_refspecs to pass
authorization through a safer environment variable or short-lived restricted git
config file, avoiding token interpolation in git -c arguments while preserving
the existing fallback and fetch behavior.

In `@scripts/diag/locate_first_trace_divergence.py`:
- Around line 9-28: Validate that the command invocation supplies both required
paths before `load(sys.argv[1])` and before accessing `sys.argv[2]`. In the
script’s entry flow, emit a clear usage/error message and terminate with a
nonzero status when the arguments are missing; preserve the existing processing
for valid invocations.

In `@scripts/diag/ppu2002_read_value_histogram.py`:
- Around line 30-36: Update the transition predicate in the row-window loop to
enforce the documented scanline, dot progression, and pre-render context in
addition to the mask pattern. Use the existing row fields represented by sls and
dots, and only append windows that satisfy the complete timing signature;
otherwise revise the output label so it does not claim these are transition
windows.

In `@scripts/diag/ppu2007_offset_greedy_search.py`:
- Around line 7-9: Update the scan_dma_abort invocation and output parsing in
the diagnostic script to check the subprocess return code and report command
failures clearly. Safely locate the STRESS2007 line, reject missing output and
the “(unset)” value with explicit diagnostics, and only call bytes.fromhex after
validating a usable value.

In `@scripts/diag/ppu2007_phase_offset_ranking.py`:
- Around line 25-28: Update the phase-5 diagnostic loop around byphase[5] and
its val calls so out-of-range samples such as val(d, -1) returning None are
handled before applying :02X formatting. Preserve hexadecimal formatting for
valid values and emit a clear diagnostic representation for missing values so
the focused output always prints.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py`:
- Around line 10-22: Update the script entry flow around the top-level
sys.argv[1] access to validate that a dump-file argument is provided before
opening it; when absent, print usage using the checked-in script’s actual
filename and exit cleanly. Ensure the documented invocation works by referencing
the script name dynamically rather than the nonexistent eval2007.py name.

In `@scripts/mapper-promotion/gen_promotion.py`:
- Around line 33-43: Reject mapper promotion entries when no staged ROM passes
the requested mapper-header validation: remove the roms[0] fallback in the
promotion loop and fail or record an error without creating a block. In
scripts/mapper-promotion/gen_promotion.py lines 59-60, derive Curated and
remaining BestEffort IDs from successfully validated blocks rather than PROMOTE.
In scripts/mapper-promotion/enumerate_staged.py lines 60-65, preserve mismatch
reporting but exclude fallback candidates from rows and promotion output.

In `@scripts/mapper-promotion/README.md`:
- Around line 3-7: The README promotion-history description incorrectly states
86 fixtures and limits them to commercial ROM dumps. Update the description to
88 distinct fixtures and characterize them as representative staged ROMs, or
explicitly document the two excluded fixtures while preserving the existing
references.

In `@scripts/pr-review/list_unresolved_threads.py`:
- Around line 5-7: The thread-printing logic must handle partial GraphQL data
safely. Update the comment extraction around t["comments"]["nodes"] to skip or
clearly report threads with no comments, and access author, databaseId, and body
defensively using the established pattern from list_all_threads.py, including a
fallback when the author is deleted or unavailable.

In `@scripts/pr-review/README.md`:
- Around line 15-16: Update the documented reviewThreads query and
list_all_threads.py behavior so the audit retrieves every thread through cursor
pagination rather than only the first 100 results. Ensure the documentation
accurately describes the implemented pagination flow and preserves the claim
that all threads are printed.

In `@tests/roms/mmc5/README.md`:
- Line 32: Update the README reference to the nonexistent
`crates/nes-test-harness/tests/m005_mmc5.rs` target so it points to the actual
MMC5 test target; if the section describes mapper implementation rather than
tests, remove the reference instead.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`:
- Around line 358-360: Update the “Next reusable-ASIC BMC/pirate cores” entry to
document the current 172 mapper IDs and use the current mNNN_<board>.rs module
pattern instead of the stale sprintN.rs path, keeping the count and source-path
metadata consistent.

In `@to-dos/ROADMAP.md`:
- Around line 339-340: Update the MMC4 history entry associated with commit
42f31ff to reference the renamed source module
crates/rustynes-mappers/src/m010_mmc4.rs instead of the obsolete combined
MMC2/MMC4 path.

---

Outside diff comments:
In `@crates/rustynes-core/src/input_device.rs`:
- Around line 267-272: Update the `sample_light` documentation to remove the
claim that per-dot temporal integration is a future refinement, and describe the
implemented `light_at_scanline`/`read_at_scanline` beam-position sampling
instead. Keep the existing explanation of frame-granular behavior only if it
remains accurate for the current implementation.

In `@docs/STATUS.md`:
- Around line 1462-1482: Remove the duplicated NTDEC/TXC/BMC phrase in the
mapper coverage summary and the repeated reusable-ASIC phrase in the v1.7.0
mapper count entry. Preserve the surrounding mapper lineage and version/count
information unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dadacf1f-ce85-4e47-ba07-b086225101ae

📥 Commits

Reviewing files that changed from the base of the PR and between af47fe6 and f8974f0.

⛔ Files ignored due to path filters (33)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snap is excluded by !**/*.snap
📒 Files selected for processing (214)
  • .github/actionlint.yaml
  • .github/release-notes/v2.2.3.md
  • .github/workflows/antigravity-review.yml
  • .github/workflows/ci.yml
  • .github/workflows/ios.yml
  • .github/workflows/pgo.yml
  • .github/workflows/release.yml
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/rustynes-apu/src/snapshot.rs
  • crates/rustynes-core/Cargo.toml
  • crates/rustynes-core/benches/full_frame.rs
  • crates/rustynes-core/benches/snapshot_restore.rs
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
  • crates/rustynes-core/src/nes.rs
  • crates/rustynes-frontend/src/app.rs
  • crates/rustynes-frontend/src/config.rs
  • crates/rustynes-frontend/src/debugger/settings_panel.rs
  • crates/rustynes-frontend/src/history_viewer.rs
  • crates/rustynes-frontend/src/runahead.rs
  • crates/rustynes-mappers/src/bmc_simple.rs
  • crates/rustynes-mappers/src/fds.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/kaiser.rs
  • crates/rustynes-mappers/src/lib.rs
  • crates/rustynes-mappers/src/m000_nrom.rs
  • crates/rustynes-mappers/src/m001_mmc1.rs
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m003_cnrom.rs
  • crates/rustynes-mappers/src/m004_mmc3.rs
  • crates/rustynes-mappers/src/m005_mmc5.rs
  • crates/rustynes-mappers/src/m007_axrom.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m010_mmc4.rs
  • crates/rustynes-mappers/src/m011_color_dreams.rs
  • crates/rustynes-mappers/src/m013_cprom.rs
  • crates/rustynes-mappers/src/m016_bandai_fcg.rs
  • crates/rustynes-mappers/src/m018_jaleco_ss88006.rs
  • crates/rustynes-mappers/src/m019_namco163.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m024_vrc6.rs
  • crates/rustynes-mappers/src/m032_irem_g101.rs
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs
  • crates/rustynes-mappers/src/m034_bnrom_nina001.rs
  • crates/rustynes-mappers/src/m035_jy_asic.rs
  • crates/rustynes-mappers/src/m036_txc_policeman.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs
  • crates/rustynes-mappers/src/m064_rambo1.rs
  • crates/rustynes-mappers/src/m065_irem_h3001.rs
  • crates/rustynes-mappers/src/m066_gxrom.rs
  • crates/rustynes-mappers/src/m067_sunsoft3.rs
  • crates/rustynes-mappers/src/m068_sunsoft4.rs
  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs
  • crates/rustynes-mappers/src/m070_bandai74.rs
  • crates/rustynes-mappers/src/m071_camerica_bf9093.rs
  • crates/rustynes-mappers/src/m073_vrc3.rs
  • crates/rustynes-mappers/src/m075_vrc1.rs
  • crates/rustynes-mappers/src/m076_namcot3446.rs
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs
  • crates/rustynes-mappers/src/m078_irem_jaleco78.rs
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs
  • crates/rustynes-mappers/src/m080_taito_x1_005.rs
  • crates/rustynes-mappers/src/m082_taito_x1_017.rs
  • crates/rustynes-mappers/src/m085_vrc7.rs
  • crates/rustynes-mappers/src/m087_jaleco87.rs
  • crates/rustynes-mappers/src/m088_namco118.rs
  • crates/rustynes-mappers/src/m089_sunsoft2.rs
  • crates/rustynes-mappers/src/m093_sunsoft3r.rs
  • crates/rustynes-mappers/src/m094_un1rom.rs
  • crates/rustynes-mappers/src/m095_namcot3425.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs
  • crates/rustynes-mappers/src/m099_vs_system.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/m113_ave_nina006.rs
  • crates/rustynes-mappers/src/m118_txsrom.rs
  • crates/rustynes-mappers/src/m119_tqrom.rs
  • crates/rustynes-mappers/src/m132_txc_22211.rs
  • crates/rustynes-mappers/src/m136_sachen_3011.rs
  • crates/rustynes-mappers/src/m151_konami_vs.rs
  • crates/rustynes-mappers/src/m152_bandai152.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs
  • crates/rustynes-mappers/src/m177_hengedianzi.rs
  • crates/rustynes-mappers/src/m179_hengedianzi.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/m184_sunsoft1.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m210_namco175.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m240_cne_multicart.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m250_nitra250.rs
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs
  • crates/rustynes-mappers/src/m513_sachen_9602.rs
  • crates/rustynes-mappers/src/mapper.rs
  • crates/rustynes-mappers/src/mmc3_clones.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs
  • crates/rustynes-mappers/src/nsf.rs
  • crates/rustynes-mappers/src/nsf_expansion.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/sprint10.rs
  • crates/rustynes-mappers/src/sprint11.rs
  • crates/rustynes-mappers/src/sprint12.rs
  • crates/rustynes-mappers/src/sprint13.rs
  • crates/rustynes-mappers/src/sprint2.rs
  • crates/rustynes-mappers/src/sprint3.rs
  • crates/rustynes-mappers/src/sprint5.rs
  • crates/rustynes-mappers/src/sprint6.rs
  • crates/rustynes-mappers/src/sprint7.rs
  • crates/rustynes-mappers/src/sprint8.rs
  • crates/rustynes-mappers/src/sprint9.rs
  • crates/rustynes-mappers/src/tier.rs
  • crates/rustynes-mappers/src/unif.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-mappers/tests/corpus.rs
  • crates/rustynes-netplay/tests/determinism.rs
  • crates/rustynes-netplay/tests/udp_loopback.rs
  • crates/rustynes-ppu/Cargo.toml
  • crates/rustynes-ppu/src/ppu.rs
  • crates/rustynes-ppu/src/snapshot.rs
  • crates/rustynes-test-harness/Cargo.toml
  • crates/rustynes-test-harness/src/bin/pgo_trainer.rs
  • crates/rustynes-test-harness/tests/accuracycoin_runahead.rs
  • crates/rustynes-test-harness/tests/audio_expansion.rs
  • crates/rustynes-test-harness/tests/cpu_reset.rs
  • crates/rustynes-test-harness/tests/fast_dotloop_diff.rs
  • crates/rustynes-test-harness/tests/holy_mapperel.rs
  • crates/rustynes-test-harness/tests/mmc3_clone_a12.rs
  • crates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rs
  • crates/rustynes-test-harness/tests/movie.rs
  • crates/rustynes-test-harness/tests/ppu_sprites.rs
  • crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
  • crates/rustynes-test-harness/tests/visual_regression.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md
  • docs/apu-2a03.md
  • docs/compatibility.md
  • docs/expansion-audio.md
  • docs/frontend.md
  • docs/mappers.md
  • docs/performance.md
  • docs/ppu-2c02.md
  • docs/testing-strategy.md
  • scripts/agy-review.sh
  • scripts/bench_relative_check.sh
  • scripts/diag/README.md
  • scripts/diag/dma_loop_span_analyzer.py
  • scripts/diag/dma_sweep_analyzer.py
  • scripts/diag/dump_trace_csv.py
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/diag/ppu2002_diff_traces.py
  • scripts/diag/ppu2002_isolated_exact_timed_reads.py
  • scripts/diag/ppu2002_prerender_vbl_reads.py
  • scripts/diag/ppu2002_read_context_window.py
  • scripts/diag/ppu2002_read_value_histogram.py
  • scripts/diag/ppu2004_diff_vs_answerkey.py
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py
  • scripts/diag/ppu2007_fetch_bus_dump.py
  • scripts/diag/ppu2007_key_alignment_shift_scan.py
  • scripts/diag/ppu2007_offset_greedy_search.py
  • scripts/diag/ppu2007_pattern_value_sets.py
  • scripts/diag/ppu2007_phase_offset_ranking.py
  • scripts/diag/ppu2007_stress_per_index_evaluator.py
  • scripts/mapper-promotion/README.md
  • scripts/mapper-promotion/batch2.py
  • scripts/mapper-promotion/convert_gg.py
  • scripts/mapper-promotion/enumerate_staged.py
  • scripts/mapper-promotion/gen_promotion.py
  • scripts/mapper-promotion/mapper_scan.py
  • scripts/mapper-promotion/promo_tests.rs
  • scripts/mapper-promotion/promo_tests_2.rs
  • scripts/mapper-promotion/scan.py
  • scripts/perf/perf_capture.sh
  • scripts/pr-review/README.md
  • scripts/pr-review/list_all_threads.py
  • scripts/pr-review/list_unresolved_threads.py
  • scripts/release-automation/split_commit.sh
  • tests/roms/LICENSES.md
  • tests/roms/README.md
  • tests/roms/assorted/README.md
  • tests/roms/assorted/apu_01_len_ctr.nes
  • tests/roms/assorted/apu_02_len_table.nes
  • tests/roms/assorted/branch_timing_1_basics.nes
  • tests/roms/assorted/branch_timing_2_backward.nes
  • tests/roms/assorted/branch_timing_3_forward.nes
  • tests/roms/assorted/cpu_reset_ram_after_reset.nes
  • tests/roms/assorted/cpu_reset_registers.nes
  • tests/roms/assorted/cpu_timing_test.nes
  • tests/roms/assorted/flowing_palette.nes
  • tests/roms/assorted/full_palette.nes
  • tests/roms/assorted/nestest.nes
  • tests/roms/assorted/oam_read.nes
  • tests/roms/assorted/oam_stress.nes
  • tests/roms/mmc5/README.md
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md
  • to-dos/ROADMAP.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (55)
.github/release-notes/v2.2.3.md (4)

19-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the actual serde migration hook.

The implementation uses #[serde(default = "default_fast_dotloop")], not bare #[serde(default)]. This distinction is load-bearing because bool::default() is false; describing it incorrectly could invite a regression that opts existing configs out of the fast path.

Suggested wording
- with a `#[serde(default)]` that defaults it **on**
+ with `#[serde(default = "default_fast_dotloop")]` so it defaults **on**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

The more important half: **the win was previously unreachable by any user.** `Nes::set_fast_dotloop` had **zero callers** outside the core and its own tests — nothing in `rustynes-frontend`, `rustynes-libretro`, or `rustynes-mobile`. It is now surfaced as an `[emulation]` config key with `#[serde(default = "default_fast_dotloop")]` so it defaults **on** for configs written before this release, so an existing `config.toml` inherits the speedup rather than silently opting out of it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 19, Update the release note to state
that the [emulation] config key uses #[serde(default = "default_fast_dotloop")]
and therefore defaults fast dotloop to enabled for existing configurations; do
not describe it as bare #[serde(default)], since that would imply
bool::default() disables the optimization.

Source: Path instructions


77-77: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Describe netplay format compatibility separately from rollback snapshots.

  • .github/release-notes/v2.2.3.md#L77-L77: state that .rnm/wire compatibility is unaffected while rollback still uses the updated snapshot state.
  • CHANGELOG.md#L213-L219: apply the same clarification.
📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L77-L77 (this comment)
  • CHANGELOG.md#L213-L219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 77, Clarify the release notes at
.github/release-notes/v2.2.3.md:77 and CHANGELOG.md:213-219 by separating
netplay format compatibility from rollback snapshot compatibility: state that
.rnm and netplay wire formats remain unaffected, while rollback snapshots use
the updated save-state schema.

Source: Path instructions


105-109: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the repeated elapsed-time calculation.

  • .github/release-notes/v2.2.3.md#L105-L109: change 28 days to 40 days, or correct the source date.
  • CHANGELOG.md#L81-L87: make the same correction.
📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L105-L109 (this comment)
  • CHANGELOG.md#L81-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 105 - 109, Correct the
elapsed-time statement describing the stale snapshots in both
.github/release-notes/v2.2.3.md lines 105-109 and CHANGELOG.md lines 81-87:
change “28 days earlier” to “40 days earlier,” or update the referenced source
date so the calculated duration is accurate.

Source: Path instructions


133-134: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## File map\n'
git ls-files '.github/release-notes/v2.2.3.md' 'CHANGELOG.md'

printf '\n## Relevant lines\n'
sed -n '125,140p' .github/release-notes/v2.2.3.md
printf '\n---\n'
sed -n '282,296p' CHANGELOG.md

printf '\n## Search for the exact clippy forms\n'
rg -n "cargo clippy|--all-targets|-D warnings| clean" .github/release-notes/v2.2.3.md CHANGELOG.md

Repository: doublegate/RustyNES

Length of output: 6585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## cargo invocation semantics probe\n'
python3 - <<'PY'
import shlex

samples = [
    "cargo clippy --workspace --all-targets -D warnings clean",
    "cargo clippy -p rustynes-mappers --no-default-features --all-targets -D warnings clean",
    "cargo clippy --workspace --all-targets -- -D warnings clean",
    "cargo clippy -p rustynes-mappers --no-default-features --all-targets -- -D warnings clean",
]
for s in samples:
    print("\nINPUT:", s)
    print("SPLIT:", shlex.split(s))
PY

Repository: doublegate/RustyNES

Length of output: 947


Move -D warnings after -- in both cargo clippy commands. .github/release-notes/v2.2.3.md#L133-L134 and CHANGELOG.md#L289-L291 should use cargo clippy … -- -D warnings so the lint flag reaches Clippy instead of Cargo.

📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L133-L134 (this comment)
  • CHANGELOG.md#L289-L291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 133 - 134, Update both cargo
clippy command entries in .github/release-notes/v2.2.3.md (lines 133-134) and
CHANGELOG.md (lines 289-291) to place -D warnings after the -- separator,
preserving the existing workspace, package, feature, and target options.

Source: Path instructions

.github/workflows/ios.yml (1)

63-77: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Select versioned Xcode bundles numerically.

Line 76 sorts version suffixes lexically, so Xcode_26.9.app wins over Xcode_26.10.app. This silently selects an older SDK once two-digit minor versions exist.

Proposed fix
-              | sort | tail -1
+              | sort -t. -k2,2n | tail -1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          #
          # Shell globbing rather than `ls -d` (shellcheck SC2012). The ordering is
          # load-bearing and unchanged: both patterns are emitted together, filtered
          # to real directories, then `sort | tail -1` picks the last. That still
          # prefers `Xcode_26.app` over `Xcode_26.<n>.app`, because after the common
          # `Xcode_26.` prefix `a` (0x61) sorts above any digit — the canonical app
          # is the lexically-highest name, not the first. A non-matching glob expands
          # to the literal pattern, which the `-d` test discards.
          xc=$(
            printf '%s\n' /Applications/Xcode_26.app /Applications/Xcode_26.*.app \
              | while IFS= read -r d; do
                  [ -d "$d" ] && printf '%s\n' "$d"
                done \
              | sort -t. -k2,2n | tail -1
          )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ios.yml around lines 63 - 77, Update the Xcode bundle
selection pipeline in the xc assignment to sort versioned Xcode_26.*.app
directories by their numeric minor suffix rather than lexical path order,
ensuring Xcode_26.10.app is selected over Xcode_26.9.app while preserving
support for the canonical Xcode_26.app bundle.
AGENTS.md (1)

188-188: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove stale v2.2.2 release-history wording.

  • AGENTS.md#L188-L188: replace “this release” with “that release” and qualify the nightly claim.
  • README.md#L806-L809: qualify the “no nightly on any build path” statement to match the documented exceptions.
📍 Affects 2 files
  • AGENTS.md#L188-L188 (this comment)
  • README.md#L806-L809
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 188, Update AGENTS.md at line 188 by changing the v2.2.2
history wording from “this release” to “that release” and qualify the claim that
no build path uses nightly. Apply the corresponding qualification to the “no
nightly on any build path” statement in README.md lines 806-809, preserving the
documented exceptions.

Source: Path instructions

CHANGELOG.md (1)

37-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- CHANGELOG excerpt ---\n'
sed -n '30,50p' CHANGELOG.md

printf '\n--- release notes excerpt ---\n'
sed -n '1,20p' .github/release-notes/v2.2.3.md

printf '\n--- search for totals in relevant files ---\n'
rg -n "2219 passed|2237 passed|AccuracyCoin holds 141/141|nestest is 0-diff|pal_apu_tests|visual_regression" CHANGELOG.md .github/release-notes/v2.2.3.md

Repository: doublegate/RustyNES

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '30,50p' CHANGELOG.md
echo
sed -n '1,20p' .github/release-notes/v2.2.3.md
echo
rg -n "2219 passed|2237 passed|AccuracyCoin holds 141/141|nestest is 0-diff|pal_apu_tests|visual_regression" CHANGELOG.md .github/release-notes/v2.2.3.md

Repository: doublegate/RustyNES

Length of output: 6334


Use one test total across both release docs. CHANGELOG.md says 2219 passed / 0 failed, while .github/release-notes/v2.2.3.md says 2237 passed / 0 failed. Pick the correct suite result and update the other copy.

📍 Affects 2 files
  • CHANGELOG.md#L37-L40 (this comment)
  • .github/release-notes/v2.2.3.md#L7-L7
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 37 - 40, Use a single authoritative test total
across both release documents: reconcile the suite result, then update the
differing entry in CHANGELOG.md (lines 37-40) or .github/release-notes/v2.2.3.md
(line 7) so both report the same passed/failed counts.

Source: Path instructions

crates/rustynes-core/src/input_device.rs (2)

1761-1772: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Latent subtraction-overflow footgun in the test helper.

fb_with_target(x, y) computes y - 1..=y + 1 and x - 1..=x + 1 on usize with no lower-bound guard; calling it with x == 0 or y == 0 would panic on overflow in debug builds. Not triggered by any current call site ((100,120), (50,60)), but worth a defensive saturating_sub for future test authors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/input_device.rs` around lines 1761 - 1772, Update
the fb_with_target test helper to prevent underflow when x or y is zero by using
saturating subtraction for the lower bounds of both pixel ranges. Preserve the
existing inclusive three-pixel neighborhood and upper-bound behavior.

1839-1846: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test doesn't actually exercise the aperture's off-screen check via the temporal path.

z.set(300, 250, ...) puts both x and y off-screen. Every tested scanline (0, 120, 239) is < y (250), so light_at_scanline short-circuits on the scanline < y beam-gate before ever reaching aperture_is_bright's ax >= W || ay >= H check. The test therefore proves the beam-gate works, not that off-screen aiming is rejected by the temporal path's aperture test — unlike zapper_temporal_rejects_lone_bright_pixel, which does reach and exercise the shared aperture logic.

🧪 Suggested fix: use an on-screen `y` with an off-screen `x` so the aperture check is actually reached
     #[test]
     fn zapper_temporal_off_screen_never_sees_light() {
         let mut z = ZapperState::new();
-        z.set(300, 250, false);
+        z.set(300, 100, false); // x off-screen, y on-screen: reaches aperture_is_bright
         let fb = alloc::vec![0xFFu8; 256 * 240 * 4];
-        for sl in [0u16, 120, 239] {
+        for sl in [100u16, 110, 121] {
             assert!(!z.light_at_scanline(&fb, sl));
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    #[test]
    fn zapper_temporal_off_screen_never_sees_light() {
        let mut z = ZapperState::new();
        z.set(300, 100, false); // x off-screen, y on-screen: reaches aperture_is_bright
        let fb = alloc::vec![0xFFu8; 256 * 240 * 4];
        for sl in [100u16, 110, 121] {
            assert!(!z.light_at_scanline(&fb, sl));
        }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/input_device.rs` around lines 1839 - 1846, Update
zapper_temporal_off_screen_never_sees_light to keep the aim’s y coordinate
on-screen while placing only x off-screen, then retain scanlines at or beyond
that y so light_at_scanline reaches aperture_is_bright’s off-screen boundary
check rather than returning through the scanline < y gate.
crates/rustynes-mappers/src/m039_subor39.rs (1)

40-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Leftover per-mapper section comments unrelated to the module's single implementation. The board-named split left standalone documentation blocks describing entirely different mappers embedded in these single-mapper files, which misleads readers about what each module implements. Remove or relocate them to the correct module.

  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52: drop the "Mapper 15 — K-1029" block (this file implements only mapper 39).
  • crates/rustynes-mappers/src/m039_subor39.rs#L192-L203: drop the "Mapper 61" block.
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43: drop the "Mapper 38 — Bit Corp" block (this file implements only mapper 41).
  • crates/rustynes-mappers/src/m041_caltron41.rs#L207-L216: drop the "Mapper 232 — Camerica" block.
📍 Affects 2 files
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52 (this comment)
  • crates/rustynes-mappers/src/m039_subor39.rs#L192-L203
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43
  • crates/rustynes-mappers/src/m041_caltron41.rs#L207-L216
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m039_subor39.rs` around lines 40 - 52, Remove the
unrelated standalone mapper documentation blocks: in
crates/rustynes-mappers/src/m039_subor39.rs lines 40-52 remove the “Mapper 15 —
K-1029” block and lines 192-203 remove the “Mapper 61” block; in
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43 remove the “Mapper 38
— Bit Corp” block and lines 207-216 remove the “Mapper 232 — Camerica” block.
Keep documentation describing each file’s actual mapper implementation.
crates/rustynes-mappers/src/m076_namcot3446.rs (2)

55-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove orphaned foreign-mapper comment blocks (and fix the joined .//! doc lines) left over from the module rename. Each of these files carries // === Mapper NN ... === banner comments describing decode protocols for mappers that are not implemented in the file, plus a doc line where a blank //! continuation got joined onto the previous sentence (...rs.//!`). Since docs are treated as the spec, these mislead readers into thinking unrelated boards live here.

  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73: delete the "Mapper 28 — Action 53" block; also delete the "Mapper 174 — NTDEC" block at L217-L225 and restore the blank doc line at L8 (...m095_namcot3425.rs\.then a separate//!`).
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L47-L59: delete the "Mapper 15" block; also delete the "Mapper 96" block at L211-L220 and split the joined doc line at L7.
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L53-L59: delete the "Mapper 38" block; also delete the "Mapper 113 — NINA-006" block at L214-L224 (that board lives in m113_ave_nina006.rs) and split the joined doc line at L11.
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71: delete the "Mapper 28 — Action 53" block; also delete the "Mapper 112 — NTDEC ASDER" block at L234-L249 and split the joined doc line at L7.
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56: delete the "Mapper 15" block; also delete the "Mapper 97 — Irem TAM-S1" block at L237-L244.
📍 Affects 5 files
  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73 (this comment)
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L47-L59
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L53-L59
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs` around lines 55 - 73, Remove
the orphaned foreign-mapper banner blocks and restore the separated blank doc
lines in crates/rustynes-mappers/src/m076_namcot3446.rs (lines 55-73, plus the
Mapper 174 block and joined line at line 8),
crates/rustynes-mappers/src/m077_irem_napoleon.rs (lines 47-59, Mapper 96 block,
and joined line at line 7), crates/rustynes-mappers/src/m079_ave_nina03_06.rs
(lines 53-59, Mapper 113 block, and joined line at line 11), and
crates/rustynes-mappers/src/m095_namcot3425.rs (lines 53-71, Mapper 112 block,
and joined line at line 7). In crates/rustynes-mappers/src/m096_bandai96.rs
(lines 44-56), also remove the Mapper 97 block; make no functional mapper
changes.

Source: Path instructions


227-228: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated #[cfg(test)] on the test module. Each of these files stacks #[cfg(test)] twice on mod tests; it compiles but is an accidental copy-paste artifact from the rename that should be de-duplicated.

  • crates/rustynes-mappers/src/m076_namcot3446.rs#L227-L228: drop the second #[cfg(test)].
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L222-L223: drop the second #[cfg(test)].
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227: drop the second #[cfg(test)].
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252: drop the second #[cfg(test)].
📍 Affects 4 files
  • crates/rustynes-mappers/src/m076_namcot3446.rs#L227-L228 (this comment)
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L222-L223
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs` around lines 227 - 228,
Remove the duplicate #[cfg(test)] attribute from the tests module in
crates/rustynes-mappers/src/m076_namcot3446.rs lines 227-228,
crates/rustynes-mappers/src/m077_irem_napoleon.rs lines 222-223,
crates/rustynes-mappers/src/m079_ave_nina03_06.rs lines 226-227, and
crates/rustynes-mappers/src/m095_namcot3425.rs lines 251-252, leaving one
attribute per module.
crates/rustynes-mappers/src/m087_jaleco87.rs (1)

10-13: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Doc prose contradicts its own formula (and the code).

Line 11 states CHR bank = (H << 0) | (L << 1), but the accompanying bit-formula on the same line and the actual implementation (line 112) both compute L<<0 | H<<1 — confirmed by the module's own bit_swapped_chr_select test (0b01 → bank 2). The code is correct; only the descriptive sentence is backwards. Since this repo's mapper docs double as the accuracy spec, leaving this contradiction risks a future edit "fixing" working code to match the wrong prose.

📝 Proposed doc fix
-//!   $6000-$7FFF [.... ..LH]  CHR bank = (H << 0) | (L << 1)
+//!   $6000-$7FFF [.... ..LH]  CHR bank = (L << 0) | (H << 1)
//!                            i.e. ((v >> 1) & 1) | ((v << 1) & 2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

//!
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m087_jaleco87.rs` around lines 10 - 13, Correct
the CHR bank mapping description in the module documentation to state that the
bank is computed as L shifted by 0 OR H shifted by 1, matching the adjacent bit
formula, the implementation, and the bit_swapped_chr_select test. Change only
the contradictory prose and preserve the existing formula and code.
crates/rustynes-mappers/src/m093_sunsoft3r.rs (1)

106-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Sunsoft-3R mapper 93 (Shanghai, Fantasy Zone) rely on bus-conflict masking for correct emulation, per nesdev/Mesen2/FCEUX source?

💡 Result:

Yes, the Sunsoft-3R board (associated with iNES mapper 93) is susceptible to bus conflicts, and emulation of this mapper requires accounting for them for accurate behavior [1][2]. The Sunsoft-2 ASIC found on the Sunsoft-3R board does not inherently prevent bus conflicts during CPU writes to the mapper registers [1]. Because these registers are mapped within the same address range as the PRG ROM ($8000–$FFFF), a write operation can cause a conflict between the CPU and the ROM output [2]. Specifically: - The MAME source code for the Sunsoft-2 device explicitly notes that the PCB is subject to bus conflict and calls a function account_bus_conflict(offset, data) to handle it [1]. - Technical documentation confirms that when two devices on the NES bus output different values, the actual result on the bus is typically a bitwise AND of those values [2]. Sunsoft games of this era, such as Shanghai, are known to intentionally account for this physical behavior (often by writing specific values to registers to ensure the resulting AND operation yields the desired configuration) [3][2]. Emulators that ignore this behavior may fail to properly bankswitch or run these games incorrectly [3][2]. While many modern emulators strive for high accuracy, the reliance on bus conflict behavior is a known characteristic of the hardware that games for mapper 93 were programmed to handle [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== m093_sunsoft3r.rs ==\n'
ast-grep outline crates/rustynes-mappers/src/m093_sunsoft3r.rs --view expanded || true
printf '\n-- relevant slice --\n'
sed -n '1,220p' crates/rustynes-mappers/src/m093_sunsoft3r.rs

printf '\n== m089_sunsoft2.rs ==\n'
ast-grep outline crates/rustynes-mappers/src/m089_sunsoft2.rs --view expanded || true
printf '\n-- relevant slice --\n'
sed -n '1,240p' crates/rustynes-mappers/src/m089_sunsoft2.rs

Repository: doublegate/RustyNES

Length of output: 18608


Mask Sunsoft-3R writes for bus conflicts.
This register is marked BUS CONFLICTS, and the Sunsoft-2 IC on mapper 89 already ANDs the write byte with the PRG-ROM byte at the same address before decoding [.PPP ...E]. Do the same here; otherwise some writes can select the wrong PRG bank or CHR-RAM enable state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m093_sunsoft3r.rs` around lines 106 - 112, Update
cpu_write to apply bus-conflict masking before decoding the Sunsoft-3R register:
AND the written value with the PRG-ROM byte read at the same address, reusing
the established mapper-89/Sunsoft-2 behavior. Decode the masked value for
prg_bank and chr_ram_enabled while preserving the existing 0x8000..=0xFFFF
address guard.
crates/rustynes-mappers/src/m094_un1rom.rs (1)

177-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Leftover sprint-file residue: dead mapper-doc blocks and duplicated #[cfg(test)].

Both newly-split files carry two artifacts from the mechanical sprint-file split: a stray block comment describing a different mapper ID that isn't implemented in that file, and a duplicated #[cfg(test)] attribute directly above mod tests. The mapper-132 block in m097_irem_tam_s1.rs is confirmed dead — that mapper already has its own file (m132_txc_23211.rs) elsewhere in this PR — and the mapper-15/mapper-101 blocks are unaccounted for in any mapper-module file list in this PR, so they're very likely the same residue. Given the PR mechanically moved ~930 top-level items, other newly split files not in this review batch may carry the same leftovers and are worth a repo-wide sweep.

  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L185: delete the dead "Mapper 101" comment block (lines 177-182) and the duplicate #[cfg(test)] (keep one).
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65: delete or relocate the "Mapper 15" comment block if mapper 15 isn't tracked elsewhere as a TODO.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L236: delete the dead "Mapper 132" comment block (lines 221-233, superseded by m132_txc_22211.rs) and the duplicate #[cfg(test)] (keep one).
#!/bin/bash
# Sweep other newly split mapper files for the same residue.
rg -n '^// ={10,}$' crates/rustynes-mappers/src -A2 | rg -B2 'Mapper \d+ —'
rg -n '#\[cfg\(test\)\]\s*\n\s*#\[cfg\(test\)\]' -U crates/rustynes-mappers/src
📍 Affects 2 files
  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L185 (this comment)
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L236
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m094_un1rom.rs` around lines 177 - 185, The split
mapper files contain leftover documentation and duplicated test attributes. In
crates/rustynes-mappers/src/m094_un1rom.rs:177-185, remove the dead Mapper 101
comment and retain only one #[cfg(test)] before mod tests; in
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65, remove or relocate the
untracked Mapper 15 comment; and in
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:221-236, remove the superseded
Mapper 132 comment and duplicate #[cfg(test)], keeping one. Sweep other newly
split mapper files for the same residue.
crates/rustynes-mappers/src/m095_namcot3425.rs (1)

150-155: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first.
git ls-files 'crates/rustynes-mappers/src/m095_namcot3425.rs' 'crates/rustynes-mappers/src/m076_namcot3446.rs'

# Show the relevant ranges with line numbers.
sed -n '1,260p' crates/rustynes-mappers/src/m095_namcot3425.rs | cat -n
printf '\n==== m076 ====\n'
sed -n '1,240p' crates/rustynes-mappers/src/m076_namcot3446.rs | cat -n

# Grep for PRG bank indexing helpers and any saturating_sub usage in mappers.
rg -n "saturating_sub|read_prg\\(|last -" crates/rustynes-mappers/src

Repository: doublegate/RustyNES

Length of output: 37096


Use saturating_sub(1) for the $C000-$DFFF window.
With exactly one 8 KiB PRG bank, last == 0, so last - 1 panics in debug builds before read_prg can wrap it back into range.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs` around lines 150 - 155,
Update the `$C000-$DFFF` branch in the `match addr` logic to compute the bank
index with saturating subtraction, ensuring `last == 0` remains valid instead of
panicking. Leave the other PRG window mappings unchanged.

Source: Coding guidelines

crates/rustynes-mappers/src/m113_ave_nina006.rs (1)

48-54: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stray section-header comments and duplicated #[cfg(test)] left over from the file split.

Lines 48-54 document "Mapper 38 — Bit Corp UNL-PCI556" and lines 208-217 document "Mapper 86 — Jaleco JF-13", but this file implements neither — mapper 38 lives in m038_bitcorp38.rs and mapper 86's Jaleco86 lives in jaleco_discrete.rs. These look like leftover section dividers from a previously combined discrete-board file that weren't removed when the boards were split out. Line 220 also duplicates #[cfg(test)] on the tests module.

Recommend deleting both stray comment blocks (or moving them to their actual destination files) and dropping the duplicate attribute, since a future reader could reasonably conclude mapper 38/86 code follows here.

🧹 Proposed cleanup
-// ===========================================================================
-// Mapper 38 — Bit Corp UNL-PCI556.
-//
-// Single 8-bit latch at $7000-$7FFF. Low 2 bits select a 32 KiB PRG bank;
-// bits 3-2 select an 8 KiB CHR bank. No bus conflicts (the register lives in
-// the $6000-$7FFF window, not in PRG-ROM). Mirroring is header-fixed; no IRQ.
-// ===========================================================================
-
 /// Mapper 113 (`NINA-006`/`MB-91` multicart).
-// ===========================================================================
-// Mapper 86 — Jaleco JF-13.
-//
-// Single latch at $6000-$6FFF (writes to $7000-$7FFF address the on-cart
-// sample-playback ADPCM, which we do not emulate). The latch byte VVdd_pPcc
-// selects:
-//   PRG = (value >> 4) & 0x03            (32 KiB)
-//   CHR = (value & 0x03) | ((value >> 4) & 0x04)   (8 KiB, 3-bit)
-// Mirroring is header-fixed; no IRQ.
-// ===========================================================================
-
-#[cfg(test)]
 #[cfg(test)]
 mod tests {

Also applies to: 208-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs` around lines 48 - 54, Remove
the stray Mapper 38 and Mapper 86 section-header comment blocks from this file,
leaving their actual implementation files unchanged. In the test module
declaration, remove the duplicated #[cfg(test)] attribute so only one remains.
crates/rustynes-mappers/src/m232_camerica_bf9096.rs (2)

38-44: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Orphaned section-header comments describe the wrong mapper. The mechanical module split left // === Mapper NNN — … === block headers in place, so they now precede unrelated implementations and actively mislead. Move each header to sit above the code it documents, or delete the stragglers. Representative sites (each module has more of the same):

  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44: "Mapper 38 — Bit Corp UNL-PCI556" precedes Camerica232 (232); remove it (and the "Mapper 240" block at L191-200).
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53: "Mapper 38" precedes Cne240 (240); remove it (and the "Mapper 241" block at L177-183).
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41: "Mapper 38" precedes Bxrom241 (241); remove it.
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59: "Mapper 28 — Action 53" precedes FongShenBang246 (246); remove it.
  • crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780: "Sachen 8259" header precedes the tests module; remove it.
  • crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301: "Mapper 36 — TXC Policeman" precedes Multicart61; realign to the code it belongs to (see also the "Mapper 72" block at L595-606).
  • crates/rustynes-mappers/src/ntdec.rs#L60-L78: "Mapper 28 — Action 53" precedes Ntdec63; realign (also L246-260, L411-422, L727-741).
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74: "Mapper 40 — NTDEC 2722" precedes helper fns/Sachen8259M137; realign (also L248-261).
  • crates/rustynes-mappers/src/sachen_discrete.rs#L1273-L1282: "Mapper 180 — Nichibutsu UNROM" precedes SachenTca01M143 (143); realign.
  • crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedes check_prg/Waixing242; realign (also L270-284, L647-656).

As per path instructions ("Docs are the spec here … Flag documentation that drifts from the code it describes").

📍 Affects 10 files
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44 (this comment)
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53
  • crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59
  • crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780
  • crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301
  • crates/rustynes-mappers/src/ntdec.rs#L60-L78
  • crates/rustynes-mappers/src/sachen_8259.rs#L58-L74
  • crates/rustynes-mappers/src/sachen_discrete.rs#L1273-L1282
  • crates/rustynes-mappers/src/waixing.rs#L63-L81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m232_camerica_bf9096.rs` around lines 38 - 44,
Remove or realign orphaned mapper section headers so each documents the correct
implementation. In crates/rustynes-mappers/src/m232_camerica_bf9096.rs:38-44 and
191-200, remove the misplaced Mapper 38 and Mapper 240 headers; do likewise for
crates/rustynes-mappers/src/m240_cne_multicart.rs:47-53 and 177-183,
m241_bxrom241.rs:35-41, m246_fong_shen_bang246.rs:41-59, and
mmc3_clones.rs:767-780. Realign or remove the headers around Multicart61 and
Mapper 72 in multicart_discrete.rs:295-301 and 595-606; Ntdec63 and the other
listed NTDEC implementations in ntdec.rs:60-78, 246-260, 411-422, and 727-741;
Sachen8259M137 and its related implementation in sachen_8259.rs:58-74 and
248-261; SachenTca01M143 in sachen_discrete.rs:1273-1282; and Waixing242 and
related implementations in waixing.rs:63-81, 270-284, and 647-656. Ensure every
remaining header matches the mapper code immediately following it.

Source: Path instructions


57-62: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

CHR doc contradicts the constructor. The # Errors doc states CHR-ROM is rejected, but new() accepts an 8 KiB CHR-ROM image and uses it read-only (lines 74-84). Align the doc with the code.

Doc fix
-    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
-    /// 16 KiB. CHR is always 8 KiB RAM (any supplied CHR-ROM is rejected).
+    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
+    /// 16 KiB, or when CHR-ROM is present but is not exactly 8 KiB. Empty CHR
+    /// yields 8 KiB CHR-RAM; an 8 KiB CHR-ROM image is accepted read-only.

As per path instructions ("Docs are the spec here … Flag documentation that drifts from the code it describes").

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    /// Construct a new mapper 232 board.
    ///
    /// # Errors
    ///
    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
    /// 16 KiB, or when CHR-ROM is present but is not exactly 8 KiB. Empty CHR
    /// yields 8 KiB CHR-RAM; an 8 KiB CHR-ROM image is accepted read-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m232_camerica_bf9096.rs` around lines 57 - 62,
Update the constructor documentation for the mapper 232 implementation near
new() so it accurately states that an 8 KiB supplied CHR-ROM is accepted and
used read-only, rather than claiming all CHR-ROM input is rejected. Keep the
existing PRG validation and CHR size requirements documented unchanged.

Source: Path instructions

crates/rustynes-mappers/src/m240_cne_multicart.rs (2)

5-5: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Malformed module doc line. The trailing //! is glued to the sentence and the paragraph break is lost, so rustdoc renders using.//! inline.

Fix
-//! window a bundled game may also be using.//!
+//! window a bundled game may also be using.
+//!
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

//! window a bundled game may also be using.
//!
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs` at line 5, Fix the
module-level documentation comment at the top of the file by separating the
trailing `//!` onto its own line, restoring the intended paragraph break so
rustdoc does not render it inline after “using.”

185-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate #[cfg(test)]. The attribute is repeated; the sibling modules carry #[allow(clippy::cast_possible_truncation)] on the second line, so this looks like a clobbered attribute. It is harmless (the file-level #![allow(...)] already covers the casts), but drop the duplicate.

Fix
 #[cfg(test)]
-#[cfg(test)]
 mod tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[cfg(test)]
mod tests {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m240_cne_multicart.rs` around lines 185 - 186,
Remove the duplicated #[cfg(test)] attribute in the test module declaration,
preserving a single #[cfg(test)] and restoring the sibling module’s
#[allow(clippy::cast_possible_truncation)] attribute on the second line if it
was clobbered.
crates/rustynes-mappers/src/multicart_discrete.rs (1)

12-20: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Module headers assert Tier-2 / MapperTier::BestEffort, contradicting tier.rs. Each header carries the same template ("A best-effort (Tier-2) board … See tier.rs (MapperTier::BestEffort)"), but tier.rs::mapper_tier classifies most (in several files, all) of the contained ids as Curated (Tier-1), and the CURATED_IDS/BEST_EFFORT_IDS tests lock that in. Qualify the header (e.g. "verified with best-effort methodology; tier per tier.rs varies by id") and drop the categorical MapperTier::BestEffort cross-reference where it is wrong.

  • crates/rustynes-mappers/src/multicart_discrete.rs#L12-L20: 15/61/62/200/201/202/203/212/213/214/225/226/227/229/231/233/234 are Curated; only 104/174/290/299/301 are BestEffort — correct the blanket claim.
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L8-L16: 246 is Curated, not BestEffort.
  • crates/rustynes-mappers/src/mmc3_clones.rs#L17-L25: 44/49/52/115/134/189/205/245 are Curated; only 238/348/366 are BestEffort.
  • crates/rustynes-mappers/src/ntdec.rs#L12-L20: 63/40/112/193/221 are Curated; 174/81/299 are BestEffort.
  • crates/rustynes-mappers/src/sachen_8259.rs#L11-L19: 137/138/139/141/156 are all Curated — the BestEffort claim is entirely wrong here.
  • crates/rustynes-mappers/src/sachen_discrete.rs#L15-L23: 133/143/145/146/147/148/149/150 are all Curated — entirely wrong.
  • crates/rustynes-mappers/src/waixing.rs#L14-L22: 162/178/242/253 are all Curated — entirely wrong.

As per path instructions ("Docs are the spec here … Flag documentation that drifts from the code it describes").

📍 Affects 7 files
  • crates/rustynes-mappers/src/multicart_discrete.rs#L12-L20 (this comment)
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L8-L16
  • crates/rustynes-mappers/src/mmc3_clones.rs#L17-L25
  • crates/rustynes-mappers/src/ntdec.rs#L12-L20
  • crates/rustynes-mappers/src/sachen_8259.rs#L11-L19
  • crates/rustynes-mappers/src/sachen_discrete.rs#L15-L23
  • crates/rustynes-mappers/src/waixing.rs#L14-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/multicart_discrete.rs` around lines 12 - 20,
Update the module headers to describe the implementations as using best-effort
verification methodology while stating that tier classification varies by mapper
id, and remove the blanket MapperTier::BestEffort reference where it is
inaccurate. Apply this to the headers in
crates/rustynes-mappers/src/multicart_discrete.rs (Curated ids
15/61/62/200/201/202/203/212/213/214/225/226/227/229/231/233/234; BestEffort ids
104/174/290/299/301), crates/rustynes-mappers/src/m246_fong_shen_bang246.rs (246
Curated), crates/rustynes-mappers/src/mmc3_clones.rs (Curated
44/49/52/115/134/189/205/245; BestEffort 238/348/366),
crates/rustynes-mappers/src/ntdec.rs (Curated 63/40/112/193/221; BestEffort
174/81/299), crates/rustynes-mappers/src/sachen_8259.rs (all Curated),
crates/rustynes-mappers/src/sachen_discrete.rs (all Curated), and
crates/rustynes-mappers/src/waixing.rs (all Curated), while preserving accurate
references to tier.rs.

Source: Path instructions

crates/rustynes-test-harness/tests/fast_dotloop_diff.rs (1)

246-316: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the is_render_scanline() gating on the $2006 / $2007 arm sites.
rg -n -A6 'deferred_copy_v = self.mask.rendering_enabled' crates/rustynes-ppu/src/ppu.rs
rg -n -B2 -A8 'ppudata_sm_countdown = n;' crates/rustynes-ppu/src/ppu.rs
rg -n 'fn is_render_scanline' -A4 crates/rustynes-ppu/src/ppu.rs

Repository: doublegate/RustyNES

Length of output: 1741


Torture-ROM overstates $2006/$2007 idle-line coverage
crates/rustynes-test-harness/tests/fast_dotloop_diff.rs:246-316$2006 and $2007 only arm copy_v_delay / ppudata_sm_countdown on render scanlines, so this vblank loop never actually drives those counters on idle-line dots. Only $2001 reliably exercises mask_write_delay here. Tighten the comment, or retime the ROM so one of those accesses lands in the last visible scanline’s fetch window and spills into idle time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-test-harness/tests/fast_dotloop_diff.rs` around lines 246 -
316, Correct vblank_io_torture_rom and its accompanying documentation so the
test no longer claims that $2006/$2007 exercise idle-line countdowns during
vblank. Either retime the ROM to place those accesses in the last visible
scanline’s fetch window and spill into idle time, or narrow the comments to
state that only $2001 reliably covers the idle-line fast-path fallback.

Source: Path instructions

crates/rustynes-test-harness/tests/holy_mapperel.rs (1)

76-81: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rename the now-closed residual section.

These lines still say both mapper families produce a nonzero WRAM nibble, contradicting the corrected detail=0000 labels and the closure text below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-test-harness/tests/holy_mapperel.rs` around lines 76 - 81,
Update the residual section heading and introductory text around the M1_* and
M69_* cases to reflect the corrected detail=0000 results, removing the claim
that both families produce a nonzero WRAM nibble. Preserve the distinction
between MMC1’s always-enabled WRAM behavior and FME-7’s modeled
RAM-enable/open-bus behavior.

Source: Path instructions

docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md (1)

156-163: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Follow-up section describes already-shipped work as a future idea.

This section frames the mechanical struct-vs-serializer diff as something merely "worth running" or "worth considering" — but crates/rustynes-test-harness/tests/snapshot_schema_audit.rs, added in this same PR, already implements exactly that as a standing test. As written, a future reader of this ADR would conclude the check doesn't exist yet.

As per path instructions, "Docs are the spec here, not a changelog. Flag documentation that drifts from the code it describes rather than just prose nits."

📝 Suggested fix
 ## Follow-up

 The audit that found this compared `struct Ppu`'s fields against the
 serializer's field list mechanically. That diff is cheap and would have
-caught all three instances of this bug class. Worth running as a check
-whenever a field is added to a chip struct, and worth considering as a test
-that fails when an unrecognized field appears in neither the schema nor an
-explicit "derived / config, deliberately not serialized" allowlist.
+caught all three instances of this bug class. This PR turns it into a
+standing test — `crates/rustynes-test-harness/tests/snapshot_schema_audit.rs`
+— that fails whenever an unrecognized field appears in neither the schema
+nor an explicit "derived / config, deliberately not serialized" allowlist,
+run on every `cargo test` invocation with no ROM and no feature gate.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

## Follow-up

The audit that found this compared `struct Ppu`'s fields against the
serializer's field list mechanically. That diff is cheap and would have
caught all three instances of this bug class. This PR turns it into a
standing test — `crates/rustynes-test-harness/tests/snapshot_schema_audit.rs`
— that fails whenever an unrecognized field appears in neither the schema
nor an explicit "derived / config, deliberately not serialized" allowlist,
run on every `cargo test` invocation with no ROM and no feature gate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md` around lines 156 - 163,
Update the “Follow-up” section of the ADR to state that the
struct-versus-serializer comparison is already implemented as a standing test in
snapshot_schema_audit.rs, rather than presenting it as future work. Preserve the
existing description of the check and its allowlist behavior while removing or
revising the speculative “worth running” and “worth considering” wording.

Source: Path instructions

docs/apu-2a03.md (1)

345-353: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reconcile both expansion-audio pages with the i32 mixer contract.

The trait was widened to i32, but both pages retain i16 contract text; docs/expansion-audio.md also still calls the 5B absolute level deferred. This misdocuments the v2.2.3 source-breaking API for out-of-tree mapper implementations.

  • docs/apu-2a03.md#L345-L353: update the earlier Mapper::mix_audio(&mut self) -> i16 signature to i32.
  • docs/expansion-audio.md#L19-L19: update the page’s hook signature and mark Sunsoft 5B absolute level as calibrated/asserted rather than deferred.

As per path instructions, “Docs are the spec here, not a changelog. Flag documentation that drifts from the code it describes.”

📍 Affects 2 files
  • docs/apu-2a03.md#L345-L353 (this comment)
  • docs/expansion-audio.md#L19-L19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/apu-2a03.md` around lines 345 - 353, Reconcile the documented
Mapper::mix_audio contract with the i32 implementation: update docs/apu-2a03.md
lines 345-353 to show an i32 return type, and update docs/expansion-audio.md
line 19 to use the i32 hook signature and describe Sunsoft 5B absolute level as
calibrated/asserted rather than deferred.

Source: Path instructions

docs/mappers.md (1)

260-262: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the renamed module references.

ave_nina.rs and cne240.rs do not match this PR’s board-named modules: use m079_ave_nina03_06.rs and m240_cne_multicart.rs, respectively.

As per path instructions, “Docs are the spec here, not a changelog. Flag documentation that drifts from the code it describes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mappers.md` around lines 260 - 262, Update the per-board module
references in the documentation list: replace ave_nina.rs with
m079_ave_nina03_06.rs and cne240.rs with m240_cne_multicart.rs, leaving the
other module names and tier description unchanged.

Source: Path instructions

scripts/agy-review.sh (1)

187-194: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bearer token passed via git -c lands in process argv.

AUTHORIZATION: bearer ${GH_TOKEN} is embedded directly in the git -c command-line argument, which is visible to any co-resident process on the runner via /proc/<pid>/cmdline / ps -ef, and would be captured verbatim by shell tracing (set -x) if ever enabled for debugging. This is a narrower but real exposure surface than passing the token via an environment variable or a short-lived, permission-restricted git config file — the same reason actions/checkout avoids this exact pattern for its own token injection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agy-review.sh` around lines 187 - 194, The fetch path exposes
GH_TOKEN in the git process arguments. Update the token-authenticated branch
around fetch_refspecs to pass authorization through a safer environment variable
or short-lived restricted git config file, avoiding token interpolation in git
-c arguments while preserving the existing fallback and fetch behavior.
scripts/diag/locate_first_trace_divergence.py (1)

9-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate command-line arguments before indexing sys.argv.

Running this without two paths raises IndexError rather than explaining the required invocation.

Proposed fix
+if len(sys.argv) != 3:
+    raise SystemExit(
+        "usage: locate_first_trace_divergence.py INPUT_TRACE.csv OUTPUT_REPORT.txt"
+    )
+
 rows = load(sys.argv[1])

As per path instructions, scripts/**/*.py prioritizes “correctness and clear error messages over style.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

if len(sys.argv) != 3:
    raise SystemExit(
        "usage: locate_first_trace_divergence.py INPUT_TRACE.csv OUTPUT_REPORT.txt"
    )

rows=load(sys.argv[1])
out=[]
out.append("rows=%d"%len(rows))
# The 4 exact-timed reads (ReadFrom2002WithExactTiming) are ~29550 cyc apart and
# each ends on a slightly later dot. Find reads whose FORWARD gap to next is in
# 29000..30500 — those are the isolated measurement reads (surrounded by a big
# clockslide). Also catch the LAST one (no big forward gap) by also taking reads
# whose BACKWARD gap is in that range.
iso=set()
for i in range(len(rows)):
    fg = rows[i+1][0]-rows[i][0] if i+1<len(rows) else 0
    bg = rows[i][0]-rows[i-1][0] if i>0 else 0
    if 29000<=fg<=30500 or 29000<=bg<=30500:
        iso.add(i)
out.append("isolated (exact-timed) reads: %d"%len(iso))
for i in sorted(iso):
    r=rows[i]
    out.append("  cyc=%d mc=%d sl=%d dot=%d val=0x%02X masked0xE0=0x%02X (V%d S%d O%d)"%(
        r[0],r[1],r[2],r[3],r[4],r[4]&0xE0,(r[4]>>7)&1,(r[4]>>6)&1,(r[4]>>5)&1))
open(sys.argv[2],'w').write("\n".join(out)+"\n")
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 27-27: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(sys.argv[2],'w')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 11-11: Use format specifiers instead of percent format

(UP031)


[warning] 23-23: Use format specifiers instead of percent format

(UP031)


[warning] 26-26: Use format specifiers instead of percent format

(UP031)


[warning] 28-28: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/locate_first_trace_divergence.py` around lines 9 - 28, Validate
that the command invocation supplies both required paths before
`load(sys.argv[1])` and before accessing `sys.argv[2]`. In the script’s entry
flow, emit a clear usage/error message and terminate with a nonzero status when
the arguments are missing; preserve the existing processing for valid
invocations.

Source: Path instructions

scripts/diag/ppu2002_read_value_histogram.py (1)

30-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Constrain transition candidates to the documented timing pattern.

The predicate only checks 0xE0 masks; it never checks the stated dot progression or pre-render context. Unrelated rows can therefore be labeled as transition windows. Include the intended scanline/dot constraints, or revise the report label.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 36-36: Do not hardcode temporary file or directory names
Context: '/tmp/RustyNES/an_out.txt'
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.15.21)

[error] 35-35: Multiple statements on one line (semicolon)

(E702)


[warning] 36-36: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 30 - 36, Update
the transition predicate in the row-window loop to enforce the documented
scanline, dot progression, and pre-render context in addition to the mask
pattern. Use the existing row fields represented by sls and dots, and only
append windows that satisfy the complete timing signature; otherwise revise the
output label so it does not claim these are transition windows.
scripts/diag/ppu2007_offset_greedy_search.py (1)

7-9: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report unusable scan_dma_abort output explicitly.

scan_dma_abort may emit STRESS2007=(unset), and command failures or a missing line currently become IndexError/ValueError. Check the return code, locate the line safely, and reject (unset) with a clear diagnostic before decoding.

Proposed fix
-    out=subprocess.run(['./target/release/scan_dma_abort',ROM,'30000'],capture_output=True,text=True,env=env).stdout
-    line=[l for l in out.splitlines() if 'STRESS2007=' in l][0].split('STRESS2007=')[1].strip()
-    data=list(bytes.fromhex(line))
+    result = subprocess.run(
+        ['./target/release/scan_dma_abort', ROM, '30000'],
+        capture_output=True,
+        text=True,
+        env=env,
+    )
+    if result.returncode:
+        raise SystemExit(result.stderr.strip() or "scan_dma_abort failed")
+    line = next(
+        (line for line in result.stdout.splitlines() if "STRESS2007=" in line),
+        None,
+    )
+    value = line.partition("STRESS2007=")[2].strip() if line else ""
+    if not value or value == "(unset)":
+        raise SystemExit("scan_dma_abort did not produce STRESS2007 data")
+    data = list(bytes.fromhex(value))

As per path instructions, one-off developer tooling should prioritize correctness and clear error messages over style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    result = subprocess.run(
        ['./target/release/scan_dma_abort', ROM, '30000'],
        capture_output=True,
        text=True,
        env=env,
    )
    if result.returncode:
        raise SystemExit(result.stderr.strip() or "scan_dma_abort failed")
    line = next(
        (line for line in result.stdout.splitlines() if "STRESS2007=" in line),
        None,
    )
    value = line.partition("STRESS2007=")[2].strip() if line else ""
    if not value or value == "(unset)":
        raise SystemExit("scan_dma_abort did not produce STRESS2007 data")
    data = list(bytes.fromhex(value))
🧰 Tools
🪛 Ruff (0.15.21)

[error] 7-7: subprocess call: check for execution of untrusted input

(S603)


[warning] 7-7: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


[warning] 8-8: Prefer next(l for l in out.splitlines() if 'STRESS2007=' in l) over single element slice

Replace with next(l for l in out.splitlines() if 'STRESS2007=' in l)

(RUF015)


[error] 8-8: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_offset_greedy_search.py` around lines 7 - 9, Update the
scan_dma_abort invocation and output parsing in the diagnostic script to check
the subprocess return code and report command failures clearly. Safely locate
the STRESS2007 line, reject missing output and the “(unset)” value with explicit
diagnostics, and only call bytes.fromhex after validating a usable value.

Source: Path instructions

scripts/diag/ppu2007_phase_offset_ranking.py (1)

25-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle out-of-range phase-5 samples before hex formatting.

On Line 28, the first phase-5 sample has d == 5; val(d, -1) returns None, so :02X raises TypeError and prevents the focused diagnostic from printing.

Proposed fix
+def fmt(value):
+    return f"{value:02X}" if value is not None else "--"
+
 for (j,d) in byphase[5][:12]:
-    print(f" j={j} dot={d} key={fullkey[j]:02X} o-1={val(d,-1):02X} o1={val(d,1):02X} o3={val(d,3):02X}")
+    print(
+        f" j={j} dot={d} key={fullkey[j]:02X}"
+        f" o-1={fmt(val(d, -1))} o1={fmt(val(d, 1))} o3={fmt(val(d, 3))}"
+    )

As per path instructions, one-off developer tooling should prioritize correctness and clear error messages over style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

# Dump phase-5 expected vs our held for first 10 tiles
print("\nphase5 (pat-lo) j,readdot,key,our@-1,our@1,our@3:")
def fmt(value):
    return f"{value:02X}" if value is not None else "--"

for (j,d) in byphase[5][:12]:
    print(
        f" j={j} dot={d} key={fullkey[j]:02X}"
        f" o-1={fmt(val(d, -1))} o1={fmt(val(d, 1))} o3={fmt(val(d, 3))}"
    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_phase_offset_ranking.py` around lines 25 - 28, Update
the phase-5 diagnostic loop around byphase[5] and its val calls so out-of-range
samples such as val(d, -1) returning None are handled before applying :02X
formatting. Preserve hexadecimal formatting for valid values and emit a clear
diagnostic representation for missing values so the focused output always
prints.

Source: Path instructions

scripts/diag/ppu2007_stress_per_index_evaluator.py (1)

10-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate arguments and print the checked-in script name.

Copying the documented command fails because eval2007.py does not exist, while invoking this script without a dump path crashes at sys.argv[1]. Emit usage and exit before opening the input.

Proposed fix
-Usage: eval2007.py <dump-file> [-v]
+Usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]
@@
 import sys
 
+if len(sys.argv) < 2:
+    print(
+        f"Usage: {os.path.basename(__file__)} <dump-file> [-v]",
+        file=sys.stderr,
+    )
+    sys.exit(2)
+
 HERE = os.path.dirname(os.path.abspath(__file__))

As per path instructions, one-off developer tooling should prioritize correctness and clear error messages over style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]
  <dump-file>: a scan_dma_abort stdout capture containing a `STRESS2007=`
               hex line, or a file of whitespace-separated hex bytes
               (the last 341 tokens are used).
  -v: print the per-index mismatch map (index, got, want, landing Y dot).
"""
import os
import sys

if len(sys.argv) < 2:
    print(
        f"Usage: {os.path.basename(__file__)} <dump-file> [-v]",
        file=sys.stderr,
    )
    sys.exit(2)

HERE = os.path.dirname(os.path.abspath(__file__))
key = [int(x, 16) for x in open(os.path.join(HERE, "key2007.txt")).read().split()]

txt = open(sys.argv[1]).read()
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 19-19: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(os.path.join(HERE, "key2007.txt"))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 21-21: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(sys.argv[1])
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 20-20: Use a context manager for opening files

(SIM115)


[warning] 22-22: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py` around lines 10 - 22,
Update the script entry flow around the top-level sys.argv[1] access to validate
that a dump-file argument is provided before opening it; when absent, print
usage using the checked-in script’s actual filename and exit cleanly. Ensure the
documented invocation works by referencing the script name dynamically rather
than the nonexistent eval2007.py name.

Source: Path instructions

scripts/mapper-promotion/gen_promotion.py (1)

33-43: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject unverified ROM candidates before generating promotion evidence.

A mapper-header mismatch currently still produces a “promotable” fixture and Curated ID. This can mark a mapper Curated using an unrelated mapper’s ROM.

  • scripts/mapper-promotion/gen_promotion.py#L33-L43: fail or record an error when no staged ROM has the requested mapper header; do not fall back to roms[0].
  • scripts/mapper-promotion/gen_promotion.py#L59-L60: derive Curated and remaining BestEffort IDs from successfully validated blocks, not the static PROMOTE list.
  • scripts/mapper-promotion/enumerate_staged.py#L60-L65: retain the mismatch report, but exclude the fallback candidate from rows and therefore from promotion output.
🧰 Tools
🪛 Ruff (0.15.21)

[error] 34-34: Multiple statements on one line (semicolon)

(E702)


[error] 39-39: Multiple statements on one line (colon)

(E701)


[error] 39-39: Multiple statements on one line (semicolon)

(E702)


[error] 40-40: Multiple statements on one line (colon)

(E701)

📍 Affects 2 files
  • scripts/mapper-promotion/gen_promotion.py#L33-L43 (this comment)
  • scripts/mapper-promotion/gen_promotion.py#L59-L60
  • scripts/mapper-promotion/enumerate_staged.py#L60-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/gen_promotion.py` around lines 33 - 43, Reject
mapper promotion entries when no staged ROM passes the requested mapper-header
validation: remove the roms[0] fallback in the promotion loop and fail or record
an error without creating a block. In scripts/mapper-promotion/gen_promotion.py
lines 59-60, derive Curated and remaining BestEffort IDs from successfully
validated blocks rather than PROMOTE. In
scripts/mapper-promotion/enumerate_staged.py lines 60-65, preserve mismatch
reporting but exclude fallback candidates from rows and promotion output.

Source: Path instructions

scripts/mapper-promotion/README.md (1)

3-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the promotion-history description.

The two checked-in generated records contain 88 distinct fixtures (58 in scripts/mapper-promotion/promo_tests.rs and 30 in scripts/mapper-promotion/promo_tests_2.rs), not 86. They also include non-commercial inputs, such as Magic Floor ... (PD) in scripts/mapper-promotion/promo_tests.rs Line 427. Update this to the actual fixture count and describe them as representative staged ROMs, or document the two excluded fixtures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/README.md` around lines 3 - 7, The README
promotion-history description incorrectly states 86 fixtures and limits them to
commercial ROM dumps. Update the description to 88 distinct fixtures and
characterize them as representative staged ROMs, or explicitly document the two
excluded fixtures while preserving the existing references.

Source: Path instructions

scripts/pr-review/list_unresolved_threads.py (1)

5-7: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle empty comments and deleted authors before indexing nested fields.

This filter can crash on valid partial GraphQL data: nodes[0] raises when no comments are returned, and c['author']['login'] fails when the reviewer account has been deleted or is unavailable. Use the defensive pattern already present in list_all_threads.py and access databaseId/body with fallbacks.

As per path instructions, one-off developer tooling should prioritize correctness and clear error messages over style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_unresolved_threads.py` around lines 5 - 7, The
thread-printing logic must handle partial GraphQL data safely. Update the
comment extraction around t["comments"]["nodes"] to skip or clearly report
threads with no comments, and access author, databaseId, and body defensively
using the established pattern from list_all_threads.py, including a fallback
when the author is deleted or unavailable.

Source: Path instructions

scripts/pr-review/README.md (1)

15-16: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Paginate reviewThreads before claiming the audit covers every thread.

reviewThreads(first:100) only supplies the first page, while the table below promises that list_all_threads.py prints every thread. A PR with more than 100 threads can therefore appear fully audited while unresolved threads remain unseen. Add cursor pagination to the query/helper, or narrow the documentation to explicitly state the 100-thread limit.

As per path instructions, Markdown documentation is the repository specification and must not drift from the behavior it describes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/README.md` around lines 15 - 16, Update the documented
reviewThreads query and list_all_threads.py behavior so the audit retrieves
every thread through cursor pagination rather than only the first 100 results.
Ensure the documentation accurately describes the implemented pagination flow
and preserves the claim that all threads are printed.

Source: Path instructions

tests/roms/mmc5/README.md (1)

32-32: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

fd -t f 'm005_mmc5.rs' crates

Repository: doublegate/RustyNES

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== README excerpt ==\n'
sed -n '24,40p' tests/roms/mmc5/README.md

printf '\n== matching files ==\n'
fd -t f 'm005_mmc5.rs' crates

printf '\n== candidate references ==\n'
rg -n "m005_mmc5\.rs|rustynes-test-harness|rustynes-mappers" tests crates -g '!**/target/**'

Repository: doublegate/RustyNES

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== crates tree (top level) ==\n'
fd -d 1 -t d '' crates | sort

printf '\n== harness-like paths ==\n'
fd -t f 'm005_mmc5.rs' .
fd -t f 'README.md' tests/roms/mmc5

Repository: doublegate/RustyNES

Length of output: 722


Fix the broken MMC5 path

crates/nes-test-harness/tests/m005_mmc5.rs does not exist. Point this README at the actual MMC5 test target, or remove the reference if it is meant to describe the mapper implementation instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/roms/mmc5/README.md` at line 32, Update the README reference to the
nonexistent `crates/nes-test-harness/tests/m005_mmc5.rs` target so it points to
the actual MMC5 test target; if the section describes mapper implementation
rather than tests, remove the reference instead.

Source: Path instructions

to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md (1)

358-360: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the mapper count and source-path metadata together.

Removing sprint12.rs from the parenthetical leaves the same entry’s “168 families on main” claim and crates/rustynes-mappers/src/sprintN.rs path stale. The current release documents 172 mapper IDs and mNNN_<board>.rs modules; update this carryover entry so it does not point to files removed by this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md` around lines 358 - 360, Update the
“Next reusable-ASIC BMC/pirate cores” entry to document the current 172 mapper
IDs and use the current mNNN_<board>.rs module pattern instead of the stale
sprintN.rs path, keeping the count and source-path metadata consistent.

Source: Path instructions

to-dos/ROADMAP.md (1)

339-340: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Point the MMC4 history at the renamed module.

The mapper reorganization separates MMC2 and MMC4 into m009_mmc2.rs and m010_mmc4.rs, but this reference still names crates/rustynes-mappers/src/mmc2_mmc4.rs. Update it to the current MMC4 source path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/ROADMAP.md` around lines 339 - 340, Update the MMC4 history entry
associated with commit 42f31ff to reference the renamed source module
crates/rustynes-mappers/src/m010_mmc4.rs instead of the obsolete combined
MMC2/MMC4 path.

Source: Path instructions

crates/rustynes-core/src/bus.rs (1)

1631-1662: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc comment on set_zapper_temporal_light is mis-copied from set_expansion_device.

Lines 1631-1637 describe attaching/replacing a per-port overlay device and carry a # Panics clause ("Panics if port is not in 0..=1"). set_zapper_temporal_light(&mut self, on: bool) takes no port and cannot panic, so the generated API docs advertise a panic that never happens. Only the A3 paragraph (1638-1653) belongs here.

Proposed doc fix
-    /// Attach (or replace) a non-standard overlay device on `port` (0 =
-    /// `$4016`, 1 = `$4017`). Pass `None` to unplug the device and return the
-    /// port to the standard controller / Four Score path (byte-identical).
-    ///
-    /// # Panics
-    ///
-    /// Panics if `port` is not in `0..=1`.
-    /// A3 (v2.2.3): enable the **beam-relative** Zapper light model.
+    /// A3 (v2.2.3): enable the **beam-relative** Zapper light model.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    /// A3 (v2.2.3): enable the **beam-relative** Zapper light model.
    ///
    /// Default **off**, which keeps the frame-granular model and therefore
    /// byte-identical output on every shipped build.
    ///
    /// With it on, the light bit is derived from where the CRT beam is at the
    /// moment of the `$4016`/`$4017` read rather than from the completed frame:
    /// dark before the beam paints the aim row, lit while the photodiode holds
    /// (~19-26 scanlines), dark once it drains. That is what real hardware
    /// does, and the frame model structurally cannot express it — it returns
    /// one answer for the whole frame.
    ///
    /// Opt-in rather than promoted because there is **no pass/fail light-gun
    /// test ROM** to adjudicate it: the supported titles re-poll every frame and
    /// are satisfied by either model, so promoting it would change output with
    /// no oracle able to confirm the change is an improvement.
    pub const fn set_zapper_temporal_light(&mut self, on: bool) {
        self.zapper_temporal_light = on;
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-core/src/bus.rs` around lines 1631 - 1662, Remove the copied
overlay-device description and its invalid # Panics clause from
set_zapper_temporal_light; retain only the A3 beam-relative Zapper light model
documentation that accurately describes the on parameter and behavior.
crates/rustynes-mappers/src/bmc_simple.rs (1)

376-385: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Untrusted dip can overflow the M286 shift on restore.

self.dip is only ever set from a save-state (load_state L486 assigns data[c+3] unmasked). With a corrupted/foreign blob, self.dip + 4 overflows the u8 add (debug panic) for dip >= 252, and 1u16 << (dip+4) overflows the shift (debug panic; release silently masks to a wrong enable mask) for dip >= 12. This is reachable from any M286 $A000 write after a bad restore, and diverges from the defensive index masking the other boards in this PR apply on restore.

Make the shift width-safe (also drops the u8 add overflow):

Proposed fix
-                    0xA000 if addr & (1u16 << (self.dip + 4)) != 0 => {
+                    0xA000 if addr & 1u16.checked_shl(u32::from(self.dip) + 4).unwrap_or(0) != 0 => {

As per coding guidelines: "do not add .unwrap(), .expect(), or panic!() when applied to untrusted data parsed from ROMs, save states...".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            SimpleBoard::M286 => {
                let bank = ((addr >> 10) & 0x03) as usize;
                match addr & 0xF000 {
                    0x8000 => self.chr2[bank] = (addr & 0x1F) as usize,
                    0xA000 if addr & 1u16.checked_shl(u32::from(self.dip) + 4).unwrap_or(0) != 0 => {
                        self.prg8[bank] = (addr & 0x0F) as usize;
                    }
                    _ => {}
                }
            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/bmc_simple.rs` around lines 376 - 385, Make the
M286 `$A000` handling in the `SimpleBoard::M286` branch safe for untrusted
`self.dip` values by converting or masking the dip index before adding the shift
offset, ensuring the resulting shift is valid for `u16`. Preserve the existing
enable-bit check and PRG bank update behavior for valid dip values, and avoid
any panic-prone operations on restored state data.

Source: Coding guidelines

crates/rustynes-mappers/src/m005_mmc5.rs (1)

1479-1481: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Do the mix arithmetic in i32, not just the final cast.

pulse_mix, pcm_mix, and MMC5_MIX_BIAS are all i16, so (pulse_mix + pcm_mix) - MMC5_MIX_BIAS is fully evaluated in i16 and only widened afterward. Today the peak (24580 pre-bias) fits, but the i32 return then buys no headroom — a later bump to MMC5_PULSE_SCALE/MMC5_PCM_SCALE would wrap (release) or panic (debug) before the widening ever helps. Promote the operands so the widened return type is actually load-bearing.

Proposed change
-        let pulse_mix = (p1 + p2) * MMC5_PULSE_SCALE; // 0..=19500
-        let pcm_mix = pcm * MMC5_PCM_SCALE; // 0..=5080
-        i32::from((pulse_mix + pcm_mix) - MMC5_MIX_BIAS)
+        let pulse_mix = i32::from(p1 + p2) * i32::from(MMC5_PULSE_SCALE); // 0..=19500
+        let pcm_mix = i32::from(pcm) * i32::from(MMC5_PCM_SCALE); // 0..=5080
+        (pulse_mix + pcm_mix) - i32::from(MMC5_MIX_BIAS)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let pulse_mix = i32::from(p1 + p2) * i32::from(MMC5_PULSE_SCALE); // 0..=19500
        let pcm_mix = i32::from(pcm) * i32::from(MMC5_PCM_SCALE); // 0..=5080
        (pulse_mix + pcm_mix) - i32::from(MMC5_MIX_BIAS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m005_mmc5.rs` around lines 1479 - 1481, Update
the mix arithmetic at the return expression to promote pulse_mix, pcm_mix, and
MMC5_MIX_BIAS to i32 before addition and subtraction, ensuring overflow cannot
occur in the intermediate i16 calculation while preserving the existing scaled
mix result.
crates/rustynes-mappers/src/m010_mmc4.rs (1)

242-286: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the core snapshot mapper WRAM (sram()) separately from Mapper::save_state?
rg -nP -C3 '\b(sram|sram_mut|save_state|load_state)\s*\(' crates/rustynes-core/src

Repository: doublegate/RustyNES

Length of output: 2045


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bus save/load sections ---'
sed -n '2700,2835p' crates/rustynes-core/src/bus.rs

echo
echo '--- mapper RAM APIs and save-state presence in m010_mmc4.rs ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m010_mmc4.rs

echo
echo '--- mapper RAM APIs and save-state presence in m021_vrc4.rs ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m021_vrc4.rs

echo
echo '--- mapper RAM APIs and save-state presence in m022_vrc2.rs ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m022_vrc2.rs

echo
echo '--- quick context for save-state on a known mapper that serializes RAM ---'
rg -n 'prg_ram|save_state|load_state' crates/rustynes-mappers/src/m005_mmc5.rs crates/rustynes-mappers/src/m018_jaleco_ss88006.rs

Repository: doublegate/RustyNES

Length of output: 16145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bus.rs: all snapshot-related tags and SRAM mentions ---'
rg -n 'save_state::tag|sram\(|sram_mut\(|write_section|read_section|SNAP|SRAM|MAP' crates/rustynes-core/src/bus.rs

echo
echo '--- m010_mmc4.rs: prg_ram / sram mentions ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m010_mmc4.rs

echo
echo '--- m021_vrc4.rs: prg_ram / sram mentions ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m021_vrc4.rs

echo
echo '--- m022_vrc2.rs: prg_ram / sram mentions ---'
rg -n 'prg_ram|sram\(|sram_mut\(|save_state|load_state' crates/rustynes-mappers/src/m022_vrc2.rs

Repository: doublegate/RustyNES

Length of output: 3601


Serialize prg_ram in these mapper snapshots. The core round-trips mapper state only through Mapper::save_state/load_state, so these boards lose WRAM on restore. Append prg_ram to the payload and restore it here, matching m005_mmc5.rs and m018_jaleco_ss88006.rs.

  • crates/rustynes-mappers/src/m010_mmc4.rs#L242-L286
  • crates/rustynes-mappers/src/m021_vrc4.rs#L401-L458
  • crates/rustynes-mappers/src/m022_vrc2.rs#L311-L347
📍 Affects 3 files
  • crates/rustynes-mappers/src/m010_mmc4.rs#L242-L286 (this comment)
  • crates/rustynes-mappers/src/m021_vrc4.rs#L401-L458
  • crates/rustynes-mappers/src/m022_vrc2.rs#L311-L347
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m010_mmc4.rs` around lines 242 - 286, Update
save_state and load_state in crates/rustynes-mappers/src/m010_mmc4.rs:242-286,
crates/rustynes-mappers/src/m021_vrc4.rs:401-458, and
crates/rustynes-mappers/src/m022_vrc2.rs:311-347 to append prg_ram after the
existing snapshot data and restore it during loading. Adjust expected lengths
and payload offsets accordingly, following the established serialization pattern
in the MMC5 and Jaleco SS88006 mappers.
crates/rustynes-mappers/src/m033_taito_tc0190.rs (1)

106-117: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how PRG-ROM sizing is enforced upstream before mapper construction.
rg -nP -C3 'prg_rom|prg\.len|is_multiple_of\(16' crates/rustynes-mappers/src/cartridge.rs
rg -nP -C3 'TaitoTc0190::new|TaitoTc0690::new' crates/rustynes-mappers/src/lib.rs

Repository: doublegate/RustyNES

Length of output: 1648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and inspect the surrounding logic.
ast-grep outline crates/rustynes-mappers/src/m033_taito_tc0190.rs --view expanded
ast-grep outline crates/rustynes-mappers/src/m048_taito_tc0690.rs --view expanded
ast-grep outline crates/rustynes-mappers/src/cartridge.rs --view expanded

printf '\n--- m033 constructor / read_prg ---\n'
sed -n '1,220p' crates/rustynes-mappers/src/m033_taito_tc0190.rs

printf '\n--- m048 constructor / read_prg ---\n'
sed -n '1,260p' crates/rustynes-mappers/src/m048_taito_tc0690.rs

printf '\n--- cartridge PRG-ROM parsing / validation ---\n'
sed -n '1,340p' crates/rustynes-mappers/src/cartridge.rs

Repository: doublegate/RustyNES

Length of output: 35363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrowly search for constructor validation and PRG bank access patterns.
rg -n -C4 'fn new\(|prg_rom.len\(\)|bank_count - 2|saturating_sub|is_multiple_of|InvalidConfig|TooSmall|prg_rom' crates/rustynes-mappers/src/m033_taito_tc0190.rs crates/rustynes-mappers/src/m048_taito_tc0690.rs crates/rustynes-mappers/src/cartridge.rs

Repository: doublegate/RustyNES

Length of output: 14483


Guard the fixed-tail PRG bank calculation
Both constructors accept any non-zero 8 KiB multiple, so a 1-bank image still reaches bank_count - 2 and panics in debug builds. Use saturating_sub(2) / saturating_sub(1) or reject < 2 PRG banks in new.

  • crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs#L132-L143
📍 Affects 2 files
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117 (this comment)
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs#L132-L143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs` around lines 106 - 117,
Guard the fixed-tail bank calculations in read_prg for
crates/rustynes-mappers/src/m033_taito_tc0190.rs lines 106-117 and
crates/rustynes-mappers/src/m048_taito_tc0690.rs lines 132-143 by using
saturating subtraction for bank_count - 2 and bank_count - 1, or reject PRG
images with fewer than two banks in each constructor; preserve valid bank
selection for multi-bank images.
crates/rustynes-mappers/src/m034_bnrom_nina001.rs (1)

190-230: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

BNROM CHR-RAM is not persisted in the save state. For the Bnrom variant chr_is_ram is true and ppu_write mutates self.chr, but neither save_state nor load_state touches self.chr. A save/restore therefore discards all CHR-RAM tile data, which breaks bit-identical restore and run-ahead determinism for BNROM titles. The sibling modules (m033, m048, m070) append chr when chr_is_ram; do the same here.

Proposed fix: serialize CHR-RAM when present
     fn save_state(&self) -> Vec<u8> {
-        let mut out = Vec::with_capacity(8 + self.prg_ram.len() + self.vram.len());
+        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
+        let mut out =
+            Vec::with_capacity(5 + self.prg_ram.len() + self.vram.len() + chr_extra);
         out.push(1u8);
         out.push(self.prg_bank);
         out.push(self.chr_bank_lo);
         out.push(self.chr_bank_hi);
         out.push(match self.variant {
             M34Variant::Bnrom => 0,
             M34Variant::Nina001 => 1,
         });
         out.extend_from_slice(&self.prg_ram);
         out.extend_from_slice(&self.vram);
+        if self.chr_is_ram {
+            out.extend_from_slice(&self.chr);
+        }
         out
     }

     fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
-        let expected = 5 + self.prg_ram.len() + self.vram.len();
+        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
+        let expected = 5 + self.prg_ram.len() + self.vram.len() + chr_extra;
         if data.len() != expected {
             return Err(MapperError::Truncated {
                 expected,
                 got: data.len(),
             });
         }
@@
         let mut cur = 5usize;
         self.prg_ram
             .copy_from_slice(&data[cur..cur + self.prg_ram.len()]);
         cur += self.prg_ram.len();
         self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
+        cur += self.vram.len();
+        if self.chr_is_ram {
+            self.chr.copy_from_slice(&data[cur..cur + self.chr.len()]);
+        }
         Ok(())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn save_state(&self) -> Vec<u8> {
        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
        let mut out =
            Vec::with_capacity(5 + self.prg_ram.len() + self.vram.len() + chr_extra);
        out.push(1u8);
        out.push(self.prg_bank);
        out.push(self.chr_bank_lo);
        out.push(self.chr_bank_hi);
        out.push(match self.variant {
            M34Variant::Bnrom => 0,
            M34Variant::Nina001 => 1,
        });
        out.extend_from_slice(&self.prg_ram);
        out.extend_from_slice(&self.vram);
        if self.chr_is_ram {
            out.extend_from_slice(&self.chr);
        }
        out
    }

    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
        let expected = 5 + self.prg_ram.len() + self.vram.len() + chr_extra;
        if data.len() != expected {
            return Err(MapperError::Truncated {
                expected,
                got: data.len(),
            });
        }
        if data[0] != 1 {
            return Err(MapperError::UnsupportedVersion(data[0]));
        }
        self.prg_bank = data[1];
        self.chr_bank_lo = data[2];
        self.chr_bank_hi = data[3];
        self.variant = match data[4] {
            0 => M34Variant::Bnrom,
            1 => M34Variant::Nina001,
            other => return Err(MapperError::Invalid(format!("variant {other}"))),
        };
        let mut cur = 5usize;
        self.prg_ram
            .copy_from_slice(&data[cur..cur + self.prg_ram.len()]);
        cur += self.prg_ram.len();
        self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
        cur += self.vram.len();
        if self.chr_is_ram {
            self.chr.copy_from_slice(&data[cur..cur + self.chr.len()]);
        }
        Ok(())
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m034_bnrom_nina001.rs` around lines 190 - 230,
Update save_state and load_state to persist self.chr when chr_is_ram is true,
matching the conditional CHR-RAM serialization used by sibling mapper modules.
Append and restore the CHR data consistently while preserving the existing state
layout for variants without CHR-RAM, and include its length in validation and
capacity calculations.
crates/rustynes-mappers/src/m036_txc_policeman.rs (2)

6-6: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run-together module-doc line renders literally as text.//!. In each file a sentence and the following blank doc line were joined, so rustdoc shows the trailing //! inside the rendered paragraph. Insert the missing blank //! line break.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6: split ...TXC accumulator chip.//! into ...TXC accumulator chip. + a separate //! line.
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L12-L12: split ...emulated in \fds.rs`.//!` the same way.
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L9-L9: split ...a naive port gets wrong.//! the same way.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L6-L6 (this comment)
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L12-L12
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L9-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` at line 6, Split the
run-together module documentation into separate lines at
crates/rustynes-mappers/src/m036_txc_policeman.rs lines 6-6,
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs lines 12-12, and
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs lines 9-9: keep each sentence
ending at its period, then add a separate blank //! line so rustdoc does not
render the marker literally.

Source: Path instructions


53-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale section-header comments describe boards/structs not present in these split modules. The mapper-crate split left banner comments from the old monolithic file; they now document unrelated mappers next to the actual implementation, which is misleading for maintainers. Remove or replace each with a header matching the code it precedes.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65: "Mapper 15 — K-1029 / Contra Function 16" block sits above Txc36 (mapper 36); delete or rewrite for mapper 36.
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L189-L196: "Mapper 39 — Subor BNROM-like" block precedes the mapper-36 tests; remove.
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167: "Mapper 79 — AVE NINA-03/06" block is unrelated to Bitcorp38; remove.
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96: "Mmc3Clone — reusable MMC3-style core" block precedes Mapper42, which is not an MMC3 clone; remove/retarget.
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L300-L310: "Mapper 50 —" block documents a board implemented in m050_fds_conv_smb2j.rs; remove.
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70: "Mmc3Clone" block precedes Mapper50 (not an MMC3 clone); remove/retarget.
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L240-L243: "DiscreteMapper — 46/51/57/…" block documents boards not in this file; remove.
📍 Affects 4 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65 (this comment)
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L189-L196
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L300-L310
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L240-L243
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` around lines 53 - 65,
Remove or retarget the stale section banners so each header describes the
implementation immediately following it: update the Mapper 15 banner above Txc36
in crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65, and remove the
unrelated Mapper 39 banner at `#L189-L196`; remove the unrelated Mapper 79 banner
in crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167; remove or retarget
the Mmc3Clone and Mapper 50 banners in
crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96 and `#L300-L310`;
and remove or retarget the Mmc3Clone and DiscreteMapper banners in
crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70 and `#L240-L243`.
crates/rustynes-mappers/src/m107_magic_dragon107.rs (1)

40-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Orphan section-comment blocks describing the wrong board. The module split left neighbouring boards' ===-banner comment blocks in files that don't implement them, so each block misdescribes the surrounding code. Remove (or replace with the correct board's) each stray block:

  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47: drop the "Mapper 31 — INL / NSF-style" banner (also the "Mapper 143 — Sachen TCA01" block at L168-175); this file is Magic Dragon 107 only.
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70: drop the "Mapper 15 — K-1029" banner (also "Mapper 133 — Sachen 3009" at L268-275).
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59: drop the "Mapper 31" banner (the "Mapper 179" block at L182-188 is the sibling but has no impl here).
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53: drop the "Mapper 38 — Bit Corp" banner (also "Mapper 250 — Nitra" at L195-207).
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58: drop the "Mapper 40 — NTDEC 2722" banner.
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451: drop the "Sachen9602 (mapper 513)" banner.
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549: drop the "Coolboy (mapper 268)" banner.
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344: drop the "TxcChip — shared by Sachen 3011" banner.
📍 Affects 8 files
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47 (this comment)
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs` around lines 40 - 47,
Remove the orphaned neighbouring-board banner comments, leaving each module’s
implementation unchanged: in crates/rustynes-mappers/src/m107_magic_dragon107.rs
at lines 40-47 and 168-175 remove the Mapper 31 and Mapper 143 blocks; in
crates/rustynes-mappers/src/m132_txc_22211.rs at lines 58-70 and 268-275 remove
the Mapper 15 and Mapper 133 blocks; in
crates/rustynes-mappers/src/m177_hengedianzi.rs at lines 52-59 and 182-188
remove the Mapper 31 and Mapper 179 blocks; in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 47-53 and 195-207
remove the Mapper 38 and Mapper 250 blocks; in
crates/rustynes-mappers/src/m250_nitra250.rs at lines 42-58 remove the Mapper 40
block; in crates/rustynes-mappers/src/m268_bmc_coolboy.rs at lines 445-451
remove the Sachen9602 block; in crates/rustynes-mappers/src/m176_bmc_fk23c.rs at
lines 541-549 remove the Coolboy block; and in
crates/rustynes-mappers/src/m513_sachen_9602.rs at lines 341-344 remove the
TxcChip block.
crates/rustynes-mappers/src/m136_sachen_3011.rs (1)

206-209: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the dispatch site's PRG-size expectations for mapper 136 and whether
# sub-32KiB (8KiB-multiple) images can reach Sachen3011::new.
rg -nP -C3 '136|new_m136|Sachen3011' crates/rustynes-mappers/src/lib.rs

Repository: doublegate/RustyNES

Length of output: 1245


PRG reads need to wrap by the actual PRG length

check_prg only requires an 8 KiB multiple, but this read wraps against count * 32 KiB. For 8/16/24 KiB PRG images, that divisor is larger than prg_rom.len(), so self.prg_rom[...] can index past the slice and panic. Wrap by self.prg_rom.len() instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs` around lines 206 - 209,
Update the 0x8000..=0xFFFF PRG read branch to wrap the computed ROM offset using
self.prg_rom.len() directly, rather than count * PRG_BANK_32K. Preserve the
existing address masking while ensuring valid indexing for all PRG lengths
accepted by check_prg.

Source: Coding guidelines

crates/rustynes-mappers/src/m156_daou156.rs (1)

42-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale per-mapper section-header comments carried into the wrong files by the module split. Each of these files contains ASCII-art header blocks describing boards that are not implemented in that file; they are residue from splitting the old monolithic module and now misdescribe the code. Remove each orphan block (or relocate it to the file that actually owns the board).

  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58: delete the Mapper 40 — NTDEC 2722 block (and the Mapper 162 — Waixing FS304 block at L229-244); the file implements only mapper 156.
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60: delete the Mapper 31 block (and the Mapper 58 block at L183-193); the file implements only mapper 179.
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58: delete the Mapper 147 — Sachen 3018 block (and the Mapper 185 — CNROM block at L215-229; mapper 185 lives in m185_cnrom185.rs).
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62: delete the Mapper 147 block (and the Mapper 200 block at L234-245); the file implements only mapper 185.
📍 Affects 4 files
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58 (this comment)
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m156_daou156.rs` around lines 42 - 58, Remove the
stale mapper header blocks from all affected files: delete the Mapper 40 and
Mapper 162 blocks in crates/rustynes-mappers/src/m156_daou156.rs#L42-L58 and its
sibling block at L229-L244; delete the Mapper 31 and Mapper 58 blocks in
crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60 and L183-L193; delete
the Mapper 147 and Mapper 185 blocks in
crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58 and L215-L229; and
delete the Mapper 147 and Mapper 200 blocks in
crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62 and L234-L245, leaving only
comments describing each file’s implemented mapper.
crates/rustynes-mappers/src/m179_hengedianzi.rs (2)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc line runs a paragraph break into prose.

...ordinary ROM write.//! glues a //! onto the sentence line, so the module doc renders write.//! literally. Split the paragraph break onto its own line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m179_hengedianzi.rs` at line 6, Update the
module-level documentation line in m179_hengedianzi.rs so the sentence ends with
“ordinary ROM write.” and the paragraph-breaking `//!` marker appears on its own
separate line, preventing it from rendering as literal prose.

195-197: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicated #[cfg(test)] attribute.

The tests module carries #[cfg(test)] twice. Harmless to the build but an obvious copy-paste artifact; drop the redundant line.

Proposed fix
-#[cfg(test)]
 #[cfg(test)]
 mod tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[cfg(test)]
mod tests {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m179_hengedianzi.rs` around lines 195 - 197,
Remove the redundant duplicate #[cfg(test)] attribute immediately before the
tests module, leaving a single conditional-compilation attribute on mod tests.
crates/rustynes-mappers/src/m210_namco175.rs (1)

296-296: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Mirroring enum declaration order / explicit discriminants.
rg -nP -A10 'enum\s+Mirroring' crates/rustynes-mappers/src/cartridge.rs

Repository: doublegate/RustyNES

Length of output: 608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cartridge.rs enum =="
cat -n crates/rustynes-mappers/src/cartridge.rs | sed -n '1,80p'

echo
echo "== namco175 save/load =="
cat -n crates/rustynes-mappers/src/m210_namco175.rs | sed -n '280,360p'

echo
echo "== sibling mirroring helpers =="
for f in crates/rustynes-mappers/src/m136_ks7058.rs \
         crates/rustynes-mappers/src/m176_mapper_176.rs \
         crates/rustynes-mappers/src/m268_mapper_268.rs \
         crates/rustynes-mappers/src/m513_mapper_513.rs; do
  echo "--- $f ---"
  rg -n "mirroring_to_byte|byte_to_mirroring|save_state|load_state|push\\(.*mirroring|pop\\(\\)" "$f"
done

Repository: doublegate/RustyNES

Length of output: 7452


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mapper files matching board names =="
fd -t f 'm*' crates/rustynes-mappers/src

echo
echo "== search for mirroring helpers across mapper sources =="
rg -n "mirroring_to_byte|byte_to_mirroring|self\.mirroring as u8|match data\[[^]]+\].*Mirroring" crates/rustynes-mappers/src

echo
echo "== board-specific files with save_state/load_state =="
rg -n "save_state|load_state" crates/rustynes-mappers/src/m* -g '*.rs'

Repository: doublegate/RustyNES

Length of output: 50375


Serialize mirroring through an explicit mapping, not the enum discriminant.

out.push(self.mirroring as u8) couples the save-state byte to Mirroring’s declaration order, while load_state already decodes through a fixed 0..5 mapping. Use explicit mirroring_to_byte / byte_to_mirroring helpers like the sibling mappers so the save format stays stable if the enum layout changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m210_namco175.rs` at line 296, Update the
save-state serialization in the mapper’s state-save method to replace the
Mirroring enum cast with the existing or added explicit mirroring_to_byte
mapping, paired with byte_to_mirroring in load_state. Preserve the fixed 0..5
save-format mapping independently of Mirroring declaration order, following the
sibling mapper implementations.

Source: Path instructions

crates/rustynes-mappers/src/m250_nitra250.rs (1)

127-137: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

last - 1 underflows for a single-8 KiB-bank PRG.

last = (len / 8 KiB).max(1) - 1, so an 8 KiB PRG gives last == 0, and the arm (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1 evaluates 0usize - 1 — a panic under overflow checks (the % count in read_prg only sanitizes the result after the subtraction). check_prg permits an 8 KiB image, and the tests use 8 banks so this is uncovered. Match the sibling boards' saturating_sub(1).

Proposed fix
-        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
+        let last = ((self.prg_rom.len() / PRG_BANK_8K).max(1) - 1);
         let r6 = self.bank_regs[6] as usize;
         let r7 = self.bank_regs[7] as usize;
         match (self.prg_mode, addr) {
             (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
-            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1,
+            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last.saturating_sub(1),
             (_, 0xA000..=0xBFFF) => r7,
             _ => last,
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn prg_bank_for(&self, addr: u16) -> usize {
        let last = ((self.prg_rom.len() / PRG_BANK_8K).max(1) - 1);
        let r6 = self.bank_regs[6] as usize;
        let r7 = self.bank_regs[7] as usize;
        match (self.prg_mode, addr) {
            (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last.saturating_sub(1),
            (_, 0xA000..=0xBFFF) => r7,
            _ => last,
        }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m250_nitra250.rs` around lines 127 - 137, Update
the prg_bank_for method’s fixed-bank match arm to use saturating subtraction
when deriving the bank before last, preventing underflow when last is zero while
preserving the existing bank selection for larger PRG images.

Source: Coding guidelines

CodeRabbit's review could not post its inline comments ("Inline review
comments failed to post ... GitHub's internal server error or limits when
posting large numbers of comments"), so all 44 findings arrived buried in
the review body and produced zero review threads. Each was verified
against the tree rather than taken on faith -- which mattered, because the
same run reported "Repository clone failed" for all four of its custom
pre-merge checks, and at least one finding is demonstrably a false
positive as a result.

CORRECTNESS -- panics reachable from a crafted-but-accepted ROM image.
All four are pre-existing (present at the merge base under the old
sprintN names; the module split moved them, CodeRabbit sees moved code as
added), and all four violate the crate's own stated invariant that "every
bank select wraps with % count, so a register write can never index out
of bounds" -- required of a #![no_std] chip stack that cannot afford a
panic on a register access.

  * m136 Sachen 3011 (the one Critical): check_prg admits any non-zero
    multiple of 8 KiB, but the read wrapped against a rounded-up
    count * PRG_BANK_32K rather than the image. A 16 KiB PRG gives
    count == 1, so $C000 resolves to offset 16384 -- one past the end of
    a 16384-byte slice, an out-of-bounds index on a plain read. Now
    `% self.prg_rom.len()`, which is what actually upholds the invariant;
    PRG_BANK_32K became unused and is dropped.
  * m032 Irem G101, m033 Taito TC0190, m048 Taito TC0690, m250 Nitra: the
    fixed-tail slots computed `bank_count - 2` / `last - 1` on a
    single-bank image, underflowing usize. Release builds survive only
    because the wrapped value lands back in range after the later
    `% count`; overflow-checked builds panic outright. Now
    `saturating_sub`, matching the idiom already used in m004_mmc3 and
    m088_namco118.

  * m093 Sunsoft-3R was missing its bus-conflict mask while its own
    module header cited nesdev INES_Mapper_093 "BUS CONFLICTS" -- the doc
    and the code disagreed. Confirmed against the designated in-tree
    reference (ref-proj/GeraNES/src/GeraNES/Mappers/Mapper093.h), whose
    writePrg opens with `data &= readPrg(addr);`. The register decode now
    ANDs with the ROM byte, via a shared read_prg helper (cpu_write needs
    &self, the trait gives &mut self). Two unit tests failed on the fix
    because synth_prg filled 0x00, which masks every register write to
    zero -- the same synthetic-ROM trap that bit the split earlier. The
    fill is now 0xFF with bank markers at offset 0 and register writes
    target $8001, matching the sibling m089_sunsoft2 convention exactly.

CORRECTNESS -- introduced by THIS pull request, unlike the above.
The A3 Zapper temporal path reached the model via
`u16::try_from(self.ppu.scanline()).unwrap_or(0)`. scanline() is i16 and
is -1 on the pre-render line, so pre-render folded onto visible row 0.
For an aim at y == 0 that row is inside the photodiode hold window, so
`light_at_scanline` passed both guards and sampled the aperture --
reporting LIGHT DETECTED during a period when the beam has painted
nothing this frame. Added ZapperState::read_before_visible() for the
case the u16 scanline cannot represent, and a regression test that pins
both halves (row 0 detects light; pre-render does not) so the negative
control cannot silently invert. The test helper also had to stop
underflowing at y == 0; it now clips the 3x3 aperture at the screen edge,
as hardware does.

SPLIT DAMAGE -- larger than the review found, and measured before acting.
CodeRabbit listed 17 orphaned "Mapper NN" section dividers describing
boards that no longer live in those files. Running the same
divider-vs-following-item check against the merge base gave 1 orphan /
72 matching, versus 57 / 25 here: the split created them, and the
detector is sound because it agrees with the old tree. 56 were removed
under an assertion that refuses to delete any line that is not a comment.
Also repaired 15 joined doc lines (a paragraph break collapsed into the
previous line, rendering a literal `//!`) and 19 duplicated `#[cfg(test)]`
attributes -- both classes my earlier repair pass had only partly caught.
No code moved; 706 mapper tests unchanged and green.

DEFERRED, DOCUMENTED -- VRC7 OPLL snapshot state.
Vrc7::save_state writes the shadow audio registers but load_state never
replays them into the live OPLL, nor restores opll_clock_counter /
last_opll_sample, so FM voice state does not survive rewind / rollback /
TAS restore. Real, audio-only, mapper-85-only, and pre-existing since the
ADR-0006 VRC7 landing. Deliberately not patched here: rustynes_apu::Opll
exposes no serialization surface at all, and the only format-free partial
fix -- replaying the registers through write_reg -- restarts every
keyed-on channel's envelope at attack, an audible transient on every
rewind, with no oracle able to say whether that is an improvement. A real
fix needs an Opll snapshot plus a mapper section v2 tail and its own
tests. Recorded as a frontier row in docs/accuracy-ledger.md instead of
rushed into a release cut.

REJECTED -- one false positive.
"Add an explicit read-only permissions block" to .github/workflows/pgo.yml:
it already declares `permissions: contents: read` at workflow level. This
is consistent with the clone failure that also made the four custom checks
inconclusive.

DOCUMENTATION -- published artifacts that contradicted the code or
themselves.

  * The release notes described the config migration as `#[serde(default)]`;
    the implementation uses `#[serde(default = "default_fast_dotloop")]`.
    The distinction is load-bearing in the opposite direction from the
    claim: plain `#[serde(default)]` yields bool's Default, false, and
    would opt every pre-v2.2.3 config out of the change the release is
    about.
  * "Movies and netplay are unaffected; both re-derive state from a fresh
    power-on" was too glib -- netplay rollback and TAS seeking do go
    through snapshot/restore. Rewritten to state the actual scope: the
    incompatibility is confined to .rns files on disk; movies carry no PPU
    snapshot; rollback/run-ahead snapshots are in-memory, always written
    by the running build, and are beneficiaries of the v8 state.
  * CHANGELOG reported 2219 for the full test-roms suite while the release
    notes reported 2237, for what reads as the same command. Both were
    true when written, at different points in the release. The CHANGELOG
    figure is now explicitly labelled as the measurement at the promotion
    commit (which is the number that matters for that item) and both
    artifacts quote the same release-wide total, re-measured on this tree:
    2238 passed / 0 failed / 20 ignored.
  * docs/performance.md's remaining-dot breakdown did not reconcile:
    20,400 + 6,820 + 341 = 27,561, not the stated 27,902. It omitted the
    post-render line and mislabelled the visible remainder as 257..=340
    when it is dot 0 plus 257..=340. All four parts are now shown summing
    to 89,342 - 61,440.
  * docs/ppu-2c02.md documented the v8 fields but not that v8 is a
    rejection epoch rather than an additive tail (ADR 0028 / ADR 0034),
    and snapshot.rs hardcoded `1..=8` beside the constant that defines it;
    the bound is now `1..=PPU_SNAPSHOT_VERSION`.
  * AGENTS.md still called v2.2.2 "this release" in two places; two stale
    mapper-module paths (sprintN.rs, mmc2_mmc4.rs) updated; the
    cpu_timing_test.nes blurb no longer claims to be "more comprehensive
    than cpu_timing_test6" when LICENSES.md identifies it as a file from
    that very suite.

TOOLING.
scripts/pr-review/list_unresolved_threads.py printed PR-sourced path,
author and body straight to the terminal with only newline stripping.
Those fields are chosen by anyone who can comment on a public PR, so an
ESC sequence could repaint the screen or fake a "resolved" line during the
closeout ceremony this script exists to drive; control characters are now
escaped, verified against a payload carrying ESC[2J. Three ad-hoc
scripts/diag helpers gained argv validation and lost a hardcoded
world-writable /tmp capture path.

Verification on this tree: cargo test --workspace --features test-roms
2238 passed / 0 failed / 20 ignored across 121 suites, with AccuracyCoin,
accuracycoin_runahead, holy_mapperel, fast_dotloop_diff (4/4),
visual_regression (9/9), snapshot_schema_audit (6/6) and nestest all
green; fmt clean; clippy --workspace --all-targets -D warnings clean;
markdownlint clean on every touched document.
@doublegate

Copy link
Copy Markdown
Owner Author

Adjudication of the CodeRabbit pass — all 44 findings

The inline comments never posted ("Inline review comments failed to post ... GitHub's
internal server error or limits when posting large numbers of comments"
), so all 44
arrived inside the review body and produced zero review threads — there is nothing to
click "Resolve" on. Disposition for each is below, and the work is in 2581ad85.

Every finding was checked against the tree rather than accepted on its face. That mattered:
the same run reported "Repository clone failed" for all four custom pre-merge checks,
and one finding turns out to be a false positive as a direct result.

Fixed — correctness

Four of these are pre-existing (present at the merge base under the old sprintN
names — the split moved the code, and CodeRabbit sees moved code as added). They are real
regardless, and each breaks the crate's own stated invariant that a register write can
never index out of bounds.

Finding Verdict
m136 Sachen 3011 OOB (Critical) Real. check_prg admits any non-zero 8 KiB multiple, but the read wrapped against count * PRG_BANK_32K. A 16 KiB PRG gives count == 1, so $C000 → offset 16384, one past a 16384-byte slice. Now % self.prg_rom.len().
m032 / m033 / m048 / m250 underflow Real. bank_count - 2 / last - 1 underflow on a single-bank image; release survives only via the later % count, overflow-checked builds panic. Now saturating_sub, matching m004_mmc3 / m088_namco118.
m093 missing bus-conflict mask Real — and the module's own header already cited nesdev INES_Mapper_093 "BUS CONFLICTS", so doc and code disagreed. Confirmed against the designated in-tree reference ref-proj/GeraNES/.../Mapper093.h (data &= readPrg(addr);).
bus.rs orphaned doc block Real — a duplicated set_expansion_device doc, including a # Panics section, sitting on a function that cannot panic.
bus.rs pre-render → row 0 Real, and introduced by this PR. scanline() is i16 (-1 on pre-render), so try_from(-1).unwrap_or(0) folded pre-render onto visible row 0 — and for an aim at y == 0 that row is inside the hold window, so it reported light detected before the beam had painted anything. Added read_before_visible() plus a regression test pinning both directions.

Worth flagging on m093: the fix broke two unit tests, because synth_prg filled 0x00,
which masks every register write to zero. That is the same synthetic-ROM trap that bit the
module split earlier in this work. The fill is now 0xFF with bank markers at offset 0 and
register writes target $8001 — the convention m089_sunsoft2 already uses.

Fixed — split damage, and there was more of it than the review found

CodeRabbit listed 17 orphaned "Mapper NN" dividers. Rather than trust or dismiss that, I ran
the same divider-vs-following-item check against the merge base: 1 orphan / 72 matching
there, versus 57 / 25 here.
The split created them, and the detector is credible precisely
because it agrees with the old tree. 56 were removed under an assertion that refuses to
delete any non-comment line. Also repaired 15 joined //! doc lines and 19
duplicated #[cfg(test)] attributes that my earlier repair pass had only partly caught.
No code moved — 706 mapper tests unchanged and green.

Fixed — documentation that contradicted the code or itself

  • Release notes said #[serde(default)]; the code uses
    #[serde(default = "default_fast_dotloop")]. The distinction cuts the opposite way from
    the claim — plain #[serde(default)] gives false and would opt every pre-v2.2.3 config
    out of the change this release is about.
  • "Movies and netplay are unaffected" was too glib: rollback and TAS seeking do use
    snapshots. Rewritten to the actual scope.
  • 2219 vs 2237. Both were true when written, at different points in the release. The
    CHANGELOG figure is now explicitly the measurement at the promotion commit, and both
    artifacts quote one re-measured release-wide total: 2238 passed / 0 failed / 20 ignored.
  • docs/performance.md's dot breakdown did not reconcile — 20,400 + 6,820 + 341 = 27,561,
    not 27,902. It omitted the post-render line and mislabelled the visible remainder. All
    four parts now sum to 89,342 − 61,440.
  • docs/ppu-2c02.md now states that v8 is a rejection epoch, not an additive tail
    (ADR 0028 / 0034), and snapshot.rs's 1..=8 literal is now 1..=PPU_SNAPSHOT_VERSION.
  • AGENTS.md "this release" ×2, two stale mapper-module paths, and the cpu_timing_test.nes
    blurb that called itself "more comprehensive than cpu_timing_test6" while LICENSES.md
    identifies it as a file from that suite.

Fixed — tooling

scripts/pr-review/list_unresolved_threads.py printed PR-sourced path, author and body to
the terminal with only newline stripping. Those fields are chosen by anyone who can comment
on a public PR, so an ESC sequence could repaint the screen or fake a "resolved" line during
the very ceremony this script drives. Control characters are now escaped (verified against a
payload carrying ESC[2J). Three scripts/diag helpers gained argv validation and lost a
hardcoded world-writable /tmp capture path.

Rejected — one false positive

pgo.yml needs a read-only permissions block. It already declares
permissions: contents: read at workflow level (lines 82-83). This is consistent with the
clone failure that also made all four custom checks inconclusive.

Deferred, with a documented ledger entry — VRC7 OPLL snapshot state

Genuine: save_state writes the shadow audio registers, but load_state never replays them
into the live OPLL and does not restore opll_clock_counter / last_opll_sample, so FM
voice state does not survive rewind / rollback / TAS restore. Audio-only, mapper-85-only, and
pre-existing since the ADR-0006 VRC7 landing — not introduced here.

Deliberately not patched in this cut. rustynes_apu::Opll exposes no serialization
surface at all, and the only format-free partial fix — replaying registers through
write_reg — restarts every keyed-on channel's envelope at attack, an audible transient on
every rewind frame, with no oracle able to adjudicate whether that is better than the status
quo. A real fix needs an Opll snapshot plus a mapper section v2 tail and its own tests.
Recorded as a frontier row in docs/accuracy-ledger.md rather than rushed into a release.

Verification on this tree

cargo test --workspace --features test-roms   2238 passed / 0 failed / 20 ignored (121 suites)
  AccuracyCoin · accuracycoin_runahead · holy_mapperel · nestest      green
  fast_dotloop_diff 4/4 · visual_regression 9/9 · snapshot_schema_audit 6/6
cargo fmt --all --check                                              clean
cargo clippy --workspace --all-targets -- -D warnings                clean
markdownlint (pre-commit, every touched document)                    clean

A note for the next run: the four custom pre-merge checks were inconclusive on a repository
clone failure, so they have still never actually executed against this PR.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

@doublegate Understood. I’m rerunning the complete review of #325 with priority on:

  • Docs-as-spec and changelog alignment for the user-visible v2.2.3 changes.
  • Panic boundaries in deserialization, mapper register decoding, and other externally influenced inputs.
  • Newly introduced or modified unsafe blocks and their safety invariants.
  • Determinism and save-state correctness in the Rust chip crates.

I’ll avoid duplicating CI-covered formatting and lint feedback unless it indicates a correctness issue.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 7 seconds.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 49

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/release-notes/v2.2.3.md:
- Line 7: Reconcile the release verification totals across
.github/release-notes/v2.2.3.md lines 7 and 123 and CHANGELOG.md lines 34-40:
update the 2237 and 2219 claims to match the recorded CI run, or explicitly
label any differing total as a pre-promotion or different-scope result, keeping
all verification blocks consistent.
- Line 107: Correct the stale interval in the release-history documentation:
update the “28 days” claim in .github/release-notes/v2.2.3.md at lines 107-107
to 40 days or state the intended reference date, and reconcile the June 13, 2026
blessing date with the July 23, 2026 release date in CHANGELOG.md at lines
84-87. Keep all release dates and measurements internally consistent.
- Line 19: Update the release-note description of the [emulation] fast-dotloop
migration to reference the implementation’s default_fast_dotloop() mechanism
instead of claiming #[serde(default)]; preserve the behavior that pre-release
configs default the setting on. In CHANGELOG.md lines 355-361, retain the
implementation-accurate wording and align .github/release-notes/v2.2.3.md line
19 with it.

In @.github/workflows/ios.yml:
- Around line 71-76: Update the Xcode candidate-filtering loop in the xc
assignment so the directory check uses an if [ -d "$d" ] conditional and only
prints matching paths. Preserve the existing sort and tail selection, allowing
no matches to reach the warning and fallback logic without failing under set
-euo pipefail.

In `@AGENTS.md`:
- Line 188: Update the release-history narrative in AGENTS.md so the v2.2.2
“Conduit” reference no longer calls it “this release”; replace that qualifier
with “the prior release” or equivalent, while preserving v2.2.3 as the current
release established elsewhere in the document.

In `@crates/rustynes-mappers/src/bmc_simple.rs`:
- Line 486: Bound the restored dip value in load_state before assigning
self.dip, preserving only its valid width so corrupted save-state data cannot
produce an invalid shift amount or overflow in cpu_write’s M286 path. Update the
self.dip restoration near the existing data[c + 3] read; do not add panic-based
handling.

In `@crates/rustynes-mappers/src/homebrew_boards.rs`:
- Around line 194-195: Remove or retarget the stale mapper banners: in
crates/rustynes-mappers/src/homebrew_boards.rs:194-195 update the banner above
MagicFloor218, 515-519 above Gtrom111, and 671-680 above Action53M28; remove the
unrelated banner at 1184-1202 above tests. In
crates/rustynes-mappers/src/m094_un1rom.rs:177-182,
crates/rustynes-mappers/src/m096_bandai96.rs:44-56 and 237-244, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65 and 221-233, remove or
retarget each banner to the following implementation or tests module.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs`:
- Around line 52-58: The section headers contain orphaned mapper descriptions
and must be corrected at every listed site: in
crates/rustynes-mappers/src/jaleco_discrete.rs lines 52-58 retitle the header
for Jaleco86/mapper 86, lines 308-322 retitle it for the
JalecoLatch/Jaleco72/Jaleco92 family, lines 631-641 retitle it for
Jaleco101/mapper 101, and lines 756-766 delete it before the test module; in
crates/rustynes-mappers/src/m250_nitra250.rs lines 42-58 retitle it for
Nitra250/mapper 250; delete the misplaced Sachen9602 header in
crates/rustynes-mappers/src/m268_bmc_coolboy.rs lines 445-451 and the misplaced
TxcChip header in crates/rustynes-mappers/src/m513_sachen_9602.rs lines 341-344.

In `@crates/rustynes-mappers/src/m009_mmc2.rs`:
- Around line 277-278: Remove the duplicated #[cfg(test)] attribute above the
tests module, leaving exactly one conditional-compilation attribute and
preserving the module’s existing test behavior.

In `@crates/rustynes-mappers/src/m010_mmc4.rs`:
- Around line 242-255: The save-state implementations for MMC4’s
save_state/load_state and VRC4’s save_state/load_state omit prg_ram. In
crates/rustynes-mappers/src/m010_mmc4.rs lines 242-255, append prg_ram after
vram, include its length in load_state’s expected size, and restore it during
loading; apply the same changes in crates/rustynes-mappers/src/m021_vrc4.rs
lines 401-418, updating the scalar_len-plus-vram expected calculation.

In `@crates/rustynes-mappers/src/m021_vrc4.rs`:
- Around line 461-462: Remove one of the duplicated #[cfg(test)] attributes
directly above the tests module, leaving a single test-configuration attribute
and preserving the module’s existing test gating.

In `@crates/rustynes-mappers/src/m022_vrc2.rs`:
- Around line 350-351: Remove the duplicate #[cfg(test)] attribute so the test
module retains exactly one conditional-compilation attribute.
- Around line 311-347: The save_state/load_state implementation must persist
prg_ram and chr_rom when chr_is_ram is enabled, in addition to the existing
mapper and VRAM fields. Bump the snapshot version tag, update the expected
serialized length and offsets consistently, and restore both buffers during load
while preserving validation and non-CHR-RAM behavior.

In `@crates/rustynes-mappers/src/m024_vrc6.rs`:
- Around line 556-561: Update the VRC6 mixing expression in the mapper’s output
method to promote p1, p2, saw, and VRC6_MIX_SCALE to i32 before performing the
subtraction and multiplication. Keep the existing centering and scaling
behavior, but ensure no intermediate arithmetic occurs in i16.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs`:
- Around line 59-60: Update the constructor rustdoc for new to state that
non-empty chr_rom must have a length that is a multiple of 2 KiB, matching
CHR_BANK_2K validation and the existing error message; leave the prg_rom and
CHR-RAM descriptions unchanged.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs`:
- Around line 53-65: Remove the leftover unrelated mapper section comments from
crates/rustynes-mappers/src/m036_txc_policeman.rs lines 53-65 and 189-196,
crates/rustynes-mappers/src/m038_bitcorp38.rs lines 161-167,
crates/rustynes-mappers/src/m039_subor39.rs lines 40-52 and 192-203,
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43 and 207-216,
crates/rustynes-mappers/src/m180_nichibutsu180.rs lines 42-58 and 215-229,
crates/rustynes-mappers/src/m185_cnrom185.rs lines 46-62 and 234-245, and
crates/rustynes-mappers/src/m232_camerica_bf9096.rs lines 38-44 and 191-200;
retain only comments describing each file’s implemented mapper.
- Line 6: Fix the crate-level documentation near the TXC accumulator chip
description by separating the trailing `//!` onto its own blank documentation
line, so the sentence ends cleanly and the crate docs render without the marker
appended.

In `@crates/rustynes-mappers/src/m069_sunsoft_fme7.rs`:
- Around line 515-534: Correct the stale tail-size annotations in the live audio
state serialization documentation: change the “21-byte tail” and “51 bytes
total” descriptions near the tail layout, plus the matching “51 bytes” reference
near the save-state format documentation, to the actual 47-byte size. Do not
modify the serialization logic or TAIL_LEN.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs`:
- Around line 55-73: Clean up stale comments and duplicated test attributes in
crates/rustynes-mappers/src/m076_namcot3446.rs (lines 55-73 and 217-228),
crates/rustynes-mappers/src/m077_irem_napoleon.rs (lines 47-59 and 211-223), and
crates/rustynes-mappers/src/m079_ave_nina03_06.rs (lines 53-59 and 214-227):
remove the unrelated mapper-description blocks, separate the jammed module
documentation comments, and remove each duplicate #[cfg(test)] attribute while
preserving the correct mapper documentation and test configuration.

In `@crates/rustynes-mappers/src/m088_namco118.rs`:
- Around line 257-260: Update Namco118::nametable_address to preserve the full 4
KiB nametable offset instead of masking with 0x07FF, so fixed four-screen mode
maps tables 2 and 3 without aliasing. Keep the existing conversion and fallback
behavior, and ensure the returned address comes from nametable_offset(addr)
unchanged within the supported u16 range.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs`:
- Around line 251-252: Remove the duplicate #[cfg(test)] attribute above the
test module, keeping exactly one attribute in
crates/rustynes-mappers/src/m095_namcot3425.rs lines 251-252,
crates/rustynes-mappers/src/m240_cne_multicart.rs lines 185-186, and
crates/rustynes-mappers/src/m244_cne_decathlon.rs lines 209-210.
- Around line 53-71: Remove the orphaned mapper documentation blocks without
changing implementation code: in crates/rustynes-mappers/src/m095_namcot3425.rs
at lines 53-71 and 234-249 remove the Mapper 28 and Mapper 112 headers; in
crates/rustynes-mappers/src/kaiser.rs at lines 603-610 remove the Waixing253
block; in crates/rustynes-mappers/src/m107_magic_dragon107.rs at lines 40-47 and
168-175 remove the Mapper 31 and Mapper 143 blocks; in
crates/rustynes-mappers/src/m240_cne_multicart.rs at lines 47-53 and 177-183
remove the Mapper 38 and Mapper 241 blocks; in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 47-53 and 195-207
remove the Mapper 38 and Mapper 250 blocks; and in
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs at lines 41-59 remove the
Mapper 28 block.
- Around line 150-155: Update the `cpu_read` PRG bank calculation so the
`0xC000..=0xDFFF` mapping uses saturating subtraction instead of `last - 1`,
preventing underflow for single-bank PRG ROMs. Preserve the existing `read_prg`
behavior and mappings for larger ROMs, and ensure untrusted ROM sizes cannot
trigger a panic.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs`:
- Around line 3-5: Update the module documentation above the mapper
implementation to describe the actual decode: bit 0 is ignored for the PRG bank,
which uses the upper bits for the 32 KiB bank, while the full byte selects the 8
KiB CHR bank. Keep the implementation unchanged.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs`:
- Around line 48-54: Remove the orphaned mapper banner comments from
crates/rustynes-mappers/src/m113_ave_nina006.rs lines 48-54 and 208-217,
crates/rustynes-mappers/src/m132_txc_22211.rs lines 58-70 and 268-275,
crates/rustynes-mappers/src/m156_daou156.rs lines 42-58 and 229-244,
crates/rustynes-mappers/src/m177_hengedianzi.rs lines 52-59, and
crates/rustynes-mappers/src/m179_hengedianzi.rs lines 53-60 and 183-193. Delete
only the banners for Mappers 38, 86, 15, 133, 40, 162, 31, 31, and 58
respectively; preserve comments describing each file’s implemented mapper.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs`:
- Around line 206-209: Update the `$8000-$FFFF` match arm in the mapper read
logic to use self.prg_rom.len() as the modulo bound, preventing out-of-bounds
indexing for valid 8 KiB and 16 KiB PRG images. Preserve the existing address
masking and bank-read behavior while replacing the 32 KiB-based count
calculation.

In `@crates/rustynes-mappers/src/m185_cnrom185.rs`:
- Around line 200-231: Update save_state and load_state to serialize chr_enabled
explicitly, preserving its exact value across snapshots including pristine
power-on state. Add the flag to the saved payload and adjust the expected length
and VRAM offset in load_state; restore chr_enabled from the serialized value
instead of recomputing it with chr_enable_for.

In `@crates/rustynes-mappers/src/m250_nitra250.rs`:
- Around line 127-137: Update prg_bank_for so the fixed-bank expression using
last - 1 saturates at zero, matching the safe behavior in
Coolboy::prg_bank_mmc3. Preserve the existing bank-selection branches while
ensuring single-bank PRG images cannot underflow or panic.

In `@crates/rustynes-mappers/src/multicart_discrete.rs`:
- Around line 1011-1015: 32 KiB PRG reads can exceed ROM bounds when the 16 KiB
bank count is odd. In Multicart202::cpu_read at
crates/rustynes-mappers/src/multicart_discrete.rs#L1011-L1015 and
Multicart212::cpu_read at
crates/rustynes-mappers/src/multicart_discrete.rs#L1380-L1383, apply the same
fix by masking the computed PRG index with self.prg_rom.len(), or calculate
banks in 32 KiB units, while preserving the existing bank and offset behavior.

In `@crates/rustynes-mappers/src/ntdec.rs`:
- Around line 817-822: Update the PRG bank calculation in the mapper read path
around self.prg_banks and last so the fixed bank for the $C000-$DFFF range
cannot underflow when only one 8 KiB bank exists. Use saturating subtraction
consistent with NtdecTc112, while preserving the existing bank mapping for
images with multiple banks.

In `@crates/rustynes-ppu/src/snapshot.rs`:
- Line 491: Update the version validation in the snapshot restore logic around
the matches! check to use PPU_SNAPSHOT_VERSION as the inclusive upper bound
instead of the hardcoded 8, preserving the existing rejection of versions
outside the supported range.

In `@crates/rustynes-test-harness/tests/holy_mapperel.rs`:
- Around line 76-83: Update the residual heading in the documentation near the
MMC1/FME-7 explanation to describe M1_* and M69_* as a closed or former
residual, consistent with the detail=0000 table and labels; remove the claim
that they currently have a nonzero WRAM nibble.

In `@crates/rustynes-test-harness/tests/snapshot_schema_audit.rs`:
- Around line 326-343: Update touches_field to validate the character
immediately before each matched “self.<field>” needle as well as the existing
trailing boundary, rejecting matches embedded in identifiers such as “myself”.
Preserve the current search loop and return true only when both boundaries are
valid.

In `@docs/expansion-audio.md`:
- Line 19: Update the Mapper::mix_audio contract documentation: in
docs/expansion-audio.md lines 19-19, change the introductory hook signature to
return i32 and replace the deferred-5B paragraph with the v2.2.3
calibrated-oracle status; in docs/apu-2a03.md lines 329-353, update the
introductory Mapper::mix_audio declaration to return i32. Treat both documents
as the current specification rather than changelog text.

In `@docs/mappers.md`:
- Around line 260-262: Update the documentation references for renamed mapper
and test-harness modules: in docs/mappers.md lines 260-262, replace ave_nina.rs
and cne240.rs with m079_ave_nina03_06.rs and m240_cne_multicart.rs; in
tests/roms/mmc5/README.md line 32, change the nes-test-harness path to
rustynes-test-harness and update the adjacent nes-mappers crate reference
accordingly.

In `@scripts/agy-review.sh`:
- Around line 186-191: Update the authenticated fetch branch in
scripts/agy-review.sh to stop passing GH_TOKEN through the git command-line -c
argument. Configure http.extraheader using GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0,
and GIT_CONFIG_VALUE_0 environment variables for the git fetch, ensuring the
bearer token is absent from process arguments while preserving the existing
fetch_refspecs behavior.

In `@scripts/bench_relative_check.sh`:
- Around line 125-132: Update the Python calculation in the benchmark comparison
around the base_ms/head_ms/delta_pct assignment to retain the full-precision
regression percentage for the MAX_REGRESSION_PCT check, while separately
formatting the displayed percentage to two decimals for printf logging. Ensure
the over comparison uses the unrounded value and preserve the existing output
format.

In `@scripts/diag/dma_loop_span_analyzer.py`:
- Around line 8-9: Remove the duplicated try structure in
scripts/diag/dma_loop_span_analyzer.py lines 8-9, retaining one try around cyc
and kind parsing with except (ValueError, IndexError). Apply the same correction
in scripts/diag/dma_sweep_analyzer.py lines 8-9, retaining one try around cyc,
kind, and held parsing with the targeted exception handler so both CSV parsers
are valid and report malformed input clearly.

In `@scripts/diag/dma_sweep_analyzer.py`:
- Around line 23-32: Update both halt-interval loops in the analyzer to test
whether prev is initialized using an explicit None check rather than truthiness.
Preserve cycle-zero as a valid prior cycle so its interval appears in the
printed output and the deltas Counter.

In `@scripts/diag/ppu2002_read_context_window.py`:
- Around line 21-24: Update the mark condition in the context-table construction
to compare each side’s scanline, dot, and value fields, including val alongside
l[2] and l[3]. Preserve the existing requirement that both l and r exist and
keep the “ <<<” output for any tuple difference.

In `@scripts/diag/ppu2002_read_value_histogram.py`:
- Around line 30-36: The transition-window predicate in the histogram loop must
enforce the documented prerender scanline, dot, and cycle timing constraints
before appending to out. Update the condition around masks and the window rows
to validate those fields from rows, while preserving the existing mask-pattern
checks and output format.

In `@scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py`:
- Around line 20-23: Update the comparison logic around the Mesen/AnswerKey1 and
Mesen/RustyNES mismatch calculations to count missing dots on each side,
explicitly report incomplete 341-dot captures, and fail when a comparison
expected to cover all 341 dots is incomplete. Do not treat absent dots as a
successful zero-mismatch comparison.

In `@scripts/diag/ppu2007_offset_greedy_search.py`:
- Around line 5-9: Update run() to validate the subprocess result and
STRESS2007= extraction before decoding: detect execution failures or missing
stress dumps, then terminate with a clear diagnostic that includes the harness
stderr. Preserve normal parsing for successful runs and avoid relying on an
unchecked list index.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py`:
- Around line 10-22: Update the usage text to name the actual evaluator script
rather than the nonexistent eval2007.py, and validate that a dump-file argument
is present before accessing sys.argv[1]. When it is missing, print the
documented usage and exit cleanly with a clear error message; preserve the
existing file-processing behavior when the argument is provided.

In `@scripts/mapper-promotion/batch2.py`:
- Around line 49-64: Update the batch processing around zip_nes and ines_mapper
so missing .nes entries or corrupt archives do not abort the loop. Catch
archive-reading failures and handle a None payload before calling ines_mapper,
recording the affected item in errors with a clear reason and continuing to the
next entry.

In `@scripts/pr-review/list_all_threads.py`:
- Line 11: Sanitize arbitrary GitHub comment text before rendering terminal
previews: update the body preview in scripts/pr-review/list_all_threads.py at
lines 11-11 and scripts/pr-review/list_unresolved_threads.py at lines 7-7 to
escape or remove control characters while preserving the existing 280- and
650-character limits.
- Around line 2-3: The GraphQL response is indexed without validating errors or
missing partial data, causing an unclear traceback. In
scripts/pr-review/list_all_threads.py lines 2-3 and
scripts/pr-review/list_unresolved_threads.py lines 2-3, validate the parsed
response before accessing repository, pullRequest, reviewThreads, or nodes;
detect GraphQL errors and absent required fields, then fail with a clear error
message instead of allowing key or type errors.

In `@scripts/pr-review/README.md`:
- Around line 15-22: Update the README’s reviewThreads GraphQL example to avoid
silently truncating results: either add pagination using pageInfo/endCursor and
retrieve all thread pages, or explicitly document that first:100 imposes a hard
100-thread limit. Ensure the closeout command’s behavior is clear for pull
requests exceeding that limit.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`:
- Around line 358-360: Update both historical Markdown pointers to use current
board-named source paths: in to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md lines
358-360, replace or remove the remaining sprintN.rs reference; in
to-dos/ROADMAP.md lines 335-338, replace mmc2_mmc4.rs with m010_mmc4.rs. Keep
all source pointers navigable after the mapper-path rename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dadacf1f-ce85-4e47-ba07-b086225101ae

📥 Commits

Reviewing files that changed from the base of the PR and between af47fe6 and f8974f0.

⛔ Files ignored due to path filters (33)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snap is excluded by !**/*.snap
  • crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snap is excluded by !**/*.snap
📒 Files selected for processing (214)
  • .github/actionlint.yaml
  • .github/release-notes/v2.2.3.md
  • .github/workflows/antigravity-review.yml
  • .github/workflows/ci.yml
  • .github/workflows/ios.yml
  • .github/workflows/pgo.yml
  • .github/workflows/release.yml
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • crates/rustynes-apu/src/snapshot.rs
  • crates/rustynes-core/Cargo.toml
  • crates/rustynes-core/benches/full_frame.rs
  • crates/rustynes-core/benches/snapshot_restore.rs
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
  • crates/rustynes-core/src/nes.rs
  • crates/rustynes-frontend/src/app.rs
  • crates/rustynes-frontend/src/config.rs
  • crates/rustynes-frontend/src/debugger/settings_panel.rs
  • crates/rustynes-frontend/src/history_viewer.rs
  • crates/rustynes-frontend/src/runahead.rs
  • crates/rustynes-mappers/src/bmc_simple.rs
  • crates/rustynes-mappers/src/fds.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/kaiser.rs
  • crates/rustynes-mappers/src/lib.rs
  • crates/rustynes-mappers/src/m000_nrom.rs
  • crates/rustynes-mappers/src/m001_mmc1.rs
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m003_cnrom.rs
  • crates/rustynes-mappers/src/m004_mmc3.rs
  • crates/rustynes-mappers/src/m005_mmc5.rs
  • crates/rustynes-mappers/src/m007_axrom.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m010_mmc4.rs
  • crates/rustynes-mappers/src/m011_color_dreams.rs
  • crates/rustynes-mappers/src/m013_cprom.rs
  • crates/rustynes-mappers/src/m016_bandai_fcg.rs
  • crates/rustynes-mappers/src/m018_jaleco_ss88006.rs
  • crates/rustynes-mappers/src/m019_namco163.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m024_vrc6.rs
  • crates/rustynes-mappers/src/m032_irem_g101.rs
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs
  • crates/rustynes-mappers/src/m034_bnrom_nina001.rs
  • crates/rustynes-mappers/src/m035_jy_asic.rs
  • crates/rustynes-mappers/src/m036_txc_policeman.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs
  • crates/rustynes-mappers/src/m064_rambo1.rs
  • crates/rustynes-mappers/src/m065_irem_h3001.rs
  • crates/rustynes-mappers/src/m066_gxrom.rs
  • crates/rustynes-mappers/src/m067_sunsoft3.rs
  • crates/rustynes-mappers/src/m068_sunsoft4.rs
  • crates/rustynes-mappers/src/m069_sunsoft_fme7.rs
  • crates/rustynes-mappers/src/m070_bandai74.rs
  • crates/rustynes-mappers/src/m071_camerica_bf9093.rs
  • crates/rustynes-mappers/src/m073_vrc3.rs
  • crates/rustynes-mappers/src/m075_vrc1.rs
  • crates/rustynes-mappers/src/m076_namcot3446.rs
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs
  • crates/rustynes-mappers/src/m078_irem_jaleco78.rs
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs
  • crates/rustynes-mappers/src/m080_taito_x1_005.rs
  • crates/rustynes-mappers/src/m082_taito_x1_017.rs
  • crates/rustynes-mappers/src/m085_vrc7.rs
  • crates/rustynes-mappers/src/m087_jaleco87.rs
  • crates/rustynes-mappers/src/m088_namco118.rs
  • crates/rustynes-mappers/src/m089_sunsoft2.rs
  • crates/rustynes-mappers/src/m093_sunsoft3r.rs
  • crates/rustynes-mappers/src/m094_un1rom.rs
  • crates/rustynes-mappers/src/m095_namcot3425.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs
  • crates/rustynes-mappers/src/m099_vs_system.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/m113_ave_nina006.rs
  • crates/rustynes-mappers/src/m118_txsrom.rs
  • crates/rustynes-mappers/src/m119_tqrom.rs
  • crates/rustynes-mappers/src/m132_txc_22211.rs
  • crates/rustynes-mappers/src/m136_sachen_3011.rs
  • crates/rustynes-mappers/src/m151_konami_vs.rs
  • crates/rustynes-mappers/src/m152_bandai152.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m176_bmc_fk23c.rs
  • crates/rustynes-mappers/src/m177_hengedianzi.rs
  • crates/rustynes-mappers/src/m179_hengedianzi.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/m184_sunsoft1.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m210_namco175.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m240_cne_multicart.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m250_nitra250.rs
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs
  • crates/rustynes-mappers/src/m513_sachen_9602.rs
  • crates/rustynes-mappers/src/mapper.rs
  • crates/rustynes-mappers/src/mmc3_clones.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs
  • crates/rustynes-mappers/src/nsf.rs
  • crates/rustynes-mappers/src/nsf_expansion.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/sprint10.rs
  • crates/rustynes-mappers/src/sprint11.rs
  • crates/rustynes-mappers/src/sprint12.rs
  • crates/rustynes-mappers/src/sprint13.rs
  • crates/rustynes-mappers/src/sprint2.rs
  • crates/rustynes-mappers/src/sprint3.rs
  • crates/rustynes-mappers/src/sprint5.rs
  • crates/rustynes-mappers/src/sprint6.rs
  • crates/rustynes-mappers/src/sprint7.rs
  • crates/rustynes-mappers/src/sprint8.rs
  • crates/rustynes-mappers/src/sprint9.rs
  • crates/rustynes-mappers/src/tier.rs
  • crates/rustynes-mappers/src/unif.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-mappers/tests/corpus.rs
  • crates/rustynes-netplay/tests/determinism.rs
  • crates/rustynes-netplay/tests/udp_loopback.rs
  • crates/rustynes-ppu/Cargo.toml
  • crates/rustynes-ppu/src/ppu.rs
  • crates/rustynes-ppu/src/snapshot.rs
  • crates/rustynes-test-harness/Cargo.toml
  • crates/rustynes-test-harness/src/bin/pgo_trainer.rs
  • crates/rustynes-test-harness/tests/accuracycoin_runahead.rs
  • crates/rustynes-test-harness/tests/audio_expansion.rs
  • crates/rustynes-test-harness/tests/cpu_reset.rs
  • crates/rustynes-test-harness/tests/fast_dotloop_diff.rs
  • crates/rustynes-test-harness/tests/holy_mapperel.rs
  • crates/rustynes-test-harness/tests/mmc3_clone_a12.rs
  • crates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rs
  • crates/rustynes-test-harness/tests/movie.rs
  • crates/rustynes-test-harness/tests/ppu_sprites.rs
  • crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
  • crates/rustynes-test-harness/tests/visual_regression.rs
  • docs/STATUS.md
  • docs/accuracy-ledger.md
  • docs/adr/0034-ppu-snapshot-v8-sprite-eval-state.md
  • docs/apu-2a03.md
  • docs/compatibility.md
  • docs/expansion-audio.md
  • docs/frontend.md
  • docs/mappers.md
  • docs/performance.md
  • docs/ppu-2c02.md
  • docs/testing-strategy.md
  • scripts/agy-review.sh
  • scripts/bench_relative_check.sh
  • scripts/diag/README.md
  • scripts/diag/dma_loop_span_analyzer.py
  • scripts/diag/dma_sweep_analyzer.py
  • scripts/diag/dump_trace_csv.py
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/diag/ppu2002_diff_traces.py
  • scripts/diag/ppu2002_isolated_exact_timed_reads.py
  • scripts/diag/ppu2002_prerender_vbl_reads.py
  • scripts/diag/ppu2002_read_context_window.py
  • scripts/diag/ppu2002_read_value_histogram.py
  • scripts/diag/ppu2004_diff_vs_answerkey.py
  • scripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.py
  • scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py
  • scripts/diag/ppu2007_fetch_bus_dump.py
  • scripts/diag/ppu2007_key_alignment_shift_scan.py
  • scripts/diag/ppu2007_offset_greedy_search.py
  • scripts/diag/ppu2007_pattern_value_sets.py
  • scripts/diag/ppu2007_phase_offset_ranking.py
  • scripts/diag/ppu2007_stress_per_index_evaluator.py
  • scripts/mapper-promotion/README.md
  • scripts/mapper-promotion/batch2.py
  • scripts/mapper-promotion/convert_gg.py
  • scripts/mapper-promotion/enumerate_staged.py
  • scripts/mapper-promotion/gen_promotion.py
  • scripts/mapper-promotion/mapper_scan.py
  • scripts/mapper-promotion/promo_tests.rs
  • scripts/mapper-promotion/promo_tests_2.rs
  • scripts/mapper-promotion/scan.py
  • scripts/perf/perf_capture.sh
  • scripts/pr-review/README.md
  • scripts/pr-review/list_all_threads.py
  • scripts/pr-review/list_unresolved_threads.py
  • scripts/release-automation/split_commit.sh
  • tests/roms/LICENSES.md
  • tests/roms/README.md
  • tests/roms/assorted/README.md
  • tests/roms/assorted/apu_01_len_ctr.nes
  • tests/roms/assorted/apu_02_len_table.nes
  • tests/roms/assorted/branch_timing_1_basics.nes
  • tests/roms/assorted/branch_timing_2_backward.nes
  • tests/roms/assorted/branch_timing_3_forward.nes
  • tests/roms/assorted/cpu_reset_ram_after_reset.nes
  • tests/roms/assorted/cpu_reset_registers.nes
  • tests/roms/assorted/cpu_timing_test.nes
  • tests/roms/assorted/flowing_palette.nes
  • tests/roms/assorted/full_palette.nes
  • tests/roms/assorted/nestest.nes
  • tests/roms/assorted/oam_read.nes
  • tests/roms/assorted/oam_stress.nes
  • tests/roms/mmc5/README.md
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md
  • to-dos/ROADMAP.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (49)
.github/release-notes/v2.2.3.md (3)

7-7: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the test totals across release records.

These entries describe the same verification with different totals. Reconcile them against the recorded CI run or label the scopes explicitly.

  • .github/release-notes/v2.2.3.md#L7-L7: correct the headline 2237 value if it is not the full-suite result.
  • .github/release-notes/v2.2.3.md#L123-L123: keep the verification block consistent with the headline.
  • CHANGELOG.md#L34-L40: update 2219 or identify it as a pre-promotion/different-scope run.

As per path instructions, Markdown is the specification, so release verification claims must agree across the release records.

📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L7-L7 (this comment)
  • .github/release-notes/v2.2.3.md#L123-L123
  • CHANGELOG.md#L34-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 7, Reconcile the release
verification totals across .github/release-notes/v2.2.3.md lines 7 and 123 and
CHANGELOG.md lines 34-40: update the 2237 and 2219 claims to match the recorded
CI run, or explicitly label any differing total as a pre-promotion or
different-scope result, keeping all verification blocks consistent.

Source: Path instructions


19-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one source of truth for config migration semantics.

  • .github/release-notes/v2.2.3.md#L19-L19: replace the #[serde(default)] claim with the actual default_fast_dotloop() mechanism.
  • CHANGELOG.md#L355-L361: retain the implementation-accurate description and align the release notes with it.

As per path instructions, Markdown is the specification, so compatibility documentation must match the implementation.

📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L19-L19 (this comment)
  • CHANGELOG.md#L355-L361
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 19, Update the release-note
description of the [emulation] fast-dotloop migration to reference the
implementation’s default_fast_dotloop() mechanism instead of claiming
#[serde(default)]; preserve the behavior that pre-release configs default the
setting on. In CHANGELOG.md lines 355-361, retain the implementation-accurate
wording and align .github/release-notes/v2.2.3.md line 19 with it.

Source: Path instructions


107-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the “28 days” claim.

  • .github/release-notes/v2.2.3.md#L107-L107: replace 28 days or state the intended reference date.
  • CHANGELOG.md#L84-L87: reconcile the June 13, 2026 blessing date with the July 23, 2026 release date; the interval is 40 days.

As per path instructions, release-history dates and measurements must remain internally consistent.

📍 Affects 2 files
  • .github/release-notes/v2.2.3.md#L107-L107 (this comment)
  • CHANGELOG.md#L84-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md at line 107, Correct the stale interval in
the release-history documentation: update the “28 days” claim in
.github/release-notes/v2.2.3.md at lines 107-107 to 40 days or state the
intended reference date, and reconcile the June 13, 2026 blessing date with the
July 23, 2026 release date in CHANGELOG.md at lines 84-87. Keep all release
dates and measurements internally consistent.

Source: Path instructions

.github/workflows/ios.yml (1)

71-76: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve the non-fatal fallback when the glob has no matches.

With set -euo pipefail, a runner with no versioned Xcode 26 app passes the literal /Applications/Xcode_26.*.app into the loop. The final [ -d "$d" ] returns 1, causing xc=$(...) to fail before the warning/fallback at Line 82. Wrap the test in if [ -d "$d" ]; then ... fi so filtered-out candidates do not fail the workflow.

Proposed fix
-                  [ -d "$d" ] && printf '%s\n' "$d"
+                  if [ -d "$d" ]; then
+                    printf '%s\n' "$d"
+                  fi

The supplied iOS readiness contract requires warning and falling back when Xcode 26 is unavailable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          xc=$(
            printf '%s\n' /Applications/Xcode_26.app /Applications/Xcode_26.*.app \
              | while IFS= read -r d; do
                  if [ -d "$d" ]; then
                    printf '%s\n' "$d"
                  fi
                done \
              | sort | tail -1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ios.yml around lines 71 - 76, Update the Xcode
candidate-filtering loop in the xc assignment so the directory check uses an if
[ -d "$d" ] conditional and only prints matching paths. Preserve the existing
sort and tail selection, allowing no matches to reach the warning and fallback
logic without failing under set -euo pipefail.
AGENTS.md (1)

188-188: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale “this release” qualifier.

Line 30 establishes v2.2.3 as current, but Line 188 still labels v2.2.2 “this release.” Replace it with “the prior release” or equivalent so agent guidance does not identify the wrong release as current.

As per path instructions, Markdown is the specification and release-state narratives must remain consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 188, Update the release-history narrative in AGENTS.md so
the v2.2.2 “Conduit” reference no longer calls it “this release”; replace that
qualifier with “the prior release” or equivalent, while preserving v2.2.3 as the
current release established elsewhere in the document.

Source: Path instructions

crates/rustynes-mappers/src/bmc_simple.rs (1)

486-486: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

dip from an untrusted save state can panic the shift in cpu_write.

load_state restores self.dip = data[c + 3] without bounding it, and the M286 path evaluates 1u16 << (self.dip + 4). A corrupt or foreign blob with dip >= 12 makes the shift amount >= 16 (panic in debug / masked-and-wrong in release), and dip >= 252 overflows the self.dip + 4 u8 add. This defeats the module's stated no-panic-on-register-access guarantee for the #![no_std] stack. dip is never written by a legitimate register, so masking it on restore to its valid width is sufficient.

Proposed fix: clamp on restore
-        self.dip = data[c + 3];
+        // `dip` selects an address-bit position for the M286 $A000 decode;
+        // bound it so a hand-edited/foreign blob can't drive `1 << (dip + 4)`
+        // past the u16 shift width (no_std no-panic invariant).
+        self.dip = data[c + 3] & 0x07;
As per coding guidelines: "do not add `.unwrap()`/`.expect()`/`panic!()` ... applied to untrusted data parsed from ... save states."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        // `dip` selects an address-bit position for the M286 $A000 decode;
        // bound it so a hand-edited/foreign blob can't drive `1 << (dip + 4)`
        // past the u16 shift width (no_std no-panic invariant).
        self.dip = data[c + 3] & 0x07;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/bmc_simple.rs` at line 486, Bound the restored
dip value in load_state before assigning self.dip, preserving only its valid
width so corrupted save-state data cannot produce an invalid shift amount or
overflow in cpu_write’s M286 path. Update the self.dip restoration near the
existing data[c + 3] read; do not add panic-based handling.

Source: Coding guidelines

crates/rustynes-mappers/src/homebrew_boards.rs (1)

194-195: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale section banners describe the wrong mapper after the file split. The sprint→board rename left // Mapper NNN — ... banner blocks that describe mappers now implemented in other files, directly above unrelated code. These actively mislead readers about what each block implements; either delete them or retarget them to the code that follows.

  • crates/rustynes-mappers/src/homebrew_boards.rs#L194-L195: banner says "Mapper 94 — UN1ROM" but the following code is MagicFloor218 (mapper 218); UN1ROM lives in m094_un1rom.rs.
  • crates/rustynes-mappers/src/homebrew_boards.rs#L515-L519: banner "Mapper 107 — Magic Dragon" precedes Gtrom111 (mapper 111).
  • crates/rustynes-mappers/src/homebrew_boards.rs#L671-L680: banner "Mapper 234 — Maxi 15" precedes Action53M28 (mapper 28).
  • crates/rustynes-mappers/src/homebrew_boards.rs#L1184-L1202: banner "Mapper 63 — NTDEC 0324" precedes the tests module; no mapper 63 exists in this file.
  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182: banner "Mapper 101 — Jaleco JF-10" precedes the tests module.
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56: banner "Mapper 15 — K-1029" precedes Bandai96 (mapper 96).
  • crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244: banner "Mapper 97 — Irem TAM-S1" precedes the tests module.
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65: banner "Mapper 15 — K-1029" precedes Irem97 (mapper 97).
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L233: banner "Mapper 132 — TXC 22211" precedes the tests module.
📍 Affects 4 files
  • crates/rustynes-mappers/src/homebrew_boards.rs#L194-L195 (this comment)
  • crates/rustynes-mappers/src/homebrew_boards.rs#L515-L519
  • crates/rustynes-mappers/src/homebrew_boards.rs#L671-L680
  • crates/rustynes-mappers/src/homebrew_boards.rs#L1184-L1202
  • crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182
  • crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56
  • crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L233
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/homebrew_boards.rs` around lines 194 - 195,
Remove or retarget the stale mapper banners: in
crates/rustynes-mappers/src/homebrew_boards.rs:194-195 update the banner above
MagicFloor218, 515-519 above Gtrom111, and 671-680 above Action53M28; remove the
unrelated banner at 1184-1202 above tests. In
crates/rustynes-mappers/src/m094_un1rom.rs:177-182,
crates/rustynes-mappers/src/m096_bandai96.rs:44-56 and 237-244, and
crates/rustynes-mappers/src/m097_irem_tam_s1.rs:53-65 and 221-233, remove or
retarget each banner to the following implementation or tests module.
crates/rustynes-mappers/src/jaleco_discrete.rs (1)

52-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Orphaned section-header comments describe mappers not in these files. The monolith→mNNN_* split carried over ===-banner comment blocks that document a mapper absent from the module they now precede, which is actively misleading when reading the adjacent register-decode/banking code. Delete or replace each with a header matching the code it introduces.

  • crates/rustynes-mappers/src/jaleco_discrete.rs#L52-L58: "Mapper 38 — Bit Corp UNL-PCI556" precedes Jaleco86 (mapper 86); remove or retitle for mapper 86.
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L308-L322: "Mapper 41 — Caltron 6-in-1" precedes the JalecoLatch/Jaleco72/Jaleco92 family; retitle for mappers 72/92.
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L631-L641: "Mapper 77 — Irem (Napoleon Senki)" precedes Jaleco101; retitle for mapper 101.
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L756-L766: "Mapper 218 — Magic Floor" precedes the #[cfg(test)] module; delete it.
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58: "Mapper 40 — NTDEC 2722" precedes Nitra250 (mapper 250); retitle for mapper 250.
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451: "Sachen9602 (mapper 513)" precedes the #[cfg(test)] module in the mapper-268 file; delete it (it belongs to m513_sachen_9602.rs).
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344: "TxcChip — TXC protection accumulator (Sachen 3011 / m136)" precedes the #[cfg(test)] module; delete it.
📍 Affects 4 files
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L52-L58 (this comment)
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L308-L322
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L631-L641
  • crates/rustynes-mappers/src/jaleco_discrete.rs#L756-L766
  • crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58
  • crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451
  • crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/jaleco_discrete.rs` around lines 52 - 58, The
section headers contain orphaned mapper descriptions and must be corrected at
every listed site: in crates/rustynes-mappers/src/jaleco_discrete.rs lines 52-58
retitle the header for Jaleco86/mapper 86, lines 308-322 retitle it for the
JalecoLatch/Jaleco72/Jaleco92 family, lines 631-641 retitle it for
Jaleco101/mapper 101, and lines 756-766 delete it before the test module; in
crates/rustynes-mappers/src/m250_nitra250.rs lines 42-58 retitle it for
Nitra250/mapper 250; delete the misplaced Sachen9602 header in
crates/rustynes-mappers/src/m268_bmc_coolboy.rs lines 445-451 and the misplaced
TxcChip header in crates/rustynes-mappers/src/m513_sachen_9602.rs lines 341-344.
crates/rustynes-mappers/src/m009_mmc2.rs (1)

277-278: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant duplicated #[cfg(test)] attribute on the tests module. Both evaluate identically, so this is a harmless copy-paste artifact; drop one.

Proposed cleanup
-#[cfg(test)]
 #[cfg(test)]
 mod tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[cfg(test)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m009_mmc2.rs` around lines 277 - 278, Remove the
duplicated #[cfg(test)] attribute above the tests module, leaving exactly one
conditional-compilation attribute and preserving the module’s existing test
behavior.
crates/rustynes-mappers/src/m010_mmc4.rs (1)

242-255: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Battery WRAM (prg_ram) is missing from the snapshot, breaking save-state determinism. Both boards wire an 8 KiB $6000-$7FFF WRAM (MMC4's own comment notes Fire Emblem Gaiden reads its save magic from it) but neither save_state/load_state serializes prg_ram. A snapshot taken mid-game and restored into a fresh mapper instance recovers zeroed WRAM instead of the saved contents — nondeterministic restore that diverges TAS/netplay-replay. m018_jaleco_ss88006.rs and m019_namco163.rs already include their WRAM in the snapshot; mirror that here (append prg_ram to the serialized tail and extend the expected length in load_state).

  • crates/rustynes-mappers/src/m010_mmc4.rs#L242-L255: append self.prg_ram after vram in save_state, add + self.prg_ram.len() to load_state's expected, and copy it back in.
  • crates/rustynes-mappers/src/m021_vrc4.rs#L401-L418: append self.prg_ram after vram in save_state, add + self.prg_ram.len() to the scalar_len + vram expected, and copy it back in.
📍 Affects 2 files
  • crates/rustynes-mappers/src/m010_mmc4.rs#L242-L255 (this comment)
  • crates/rustynes-mappers/src/m021_vrc4.rs#L401-L418
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m010_mmc4.rs` around lines 242 - 255, The
save-state implementations for MMC4’s save_state/load_state and VRC4’s
save_state/load_state omit prg_ram. In crates/rustynes-mappers/src/m010_mmc4.rs
lines 242-255, append prg_ram after vram, include its length in load_state’s
expected size, and restore it during loading; apply the same changes in
crates/rustynes-mappers/src/m021_vrc4.rs lines 401-418, updating the
scalar_len-plus-vram expected calculation.
crates/rustynes-mappers/src/m021_vrc4.rs (1)

461-462: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant duplicated #[cfg(test)] attribute on the tests module (same artifact as m009_mmc2.rs). Drop one.

Proposed cleanup
-#[cfg(test)]
 #[cfg(test)]
 mod tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[cfg(test)]
mod tests {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m021_vrc4.rs` around lines 461 - 462, Remove one
of the duplicated #[cfg(test)] attributes directly above the tests module,
leaving a single test-configuration attribute and preserving the module’s
existing test gating.
crates/rustynes-mappers/src/m022_vrc2.rs (2)

311-347: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=rust -C3 '\b(save_state|load_state|sram(_mut)?)\b' crates/rustynes-mappers/src/m022_vrc2.rs
# Compare with how a sibling serializes prg_ram + CHR-RAM
rg -nP --type=rust -C2 'chr_is_ram|prg_ram' crates/rustynes-mappers/src/m000_nrom.rs

Repository: doublegate/RustyNES

Length of output: 4322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the mapper trait and snapshot call sites.
rg -n -C3 'fn (save_state|load_state|sram|sram_mut)\b|save_state\(|load_state\(' crates/rustynes-mappers crates/rustynes-core crates/rustynes-*/src | sed -n '1,220p'

# Show the Vrc2 struct fields and its save/load implementations.
sed -n '1,120p' crates/rustynes-mappers/src/m022_vrc2.rs
printf '\n----\n'
sed -n '300,360p' crates/rustynes-mappers/src/m022_vrc2.rs

Repository: doublegate/RustyNES

Length of output: 24014


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find whether battery-backed SRAM is serialized outside the mapper snapshot blob.
rg -n -C2 'sram_mut\(|sram\(\)|battery|save.*sram|load.*sram|persist.*sram|mapper.*save_state' crates/rustynes-core crates/rustynes-mappers crates/rustynes-*/src

Repository: doublegate/RustyNES

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any core-level snapshot code that stores mapper SRAM separately.
rg -n -C3 'sram\(\)|sram_mut\(\)|save_state\(|load_state\(|snapshot' crates/rustynes-core crates/rustynes-mobile crates/rustynes-mappers/src/mapper.rs crates/rustynes-mappers/src/lib.rs | sed -n '1,240p'

# Show the Mapper trait methods around save/load/sram.
sed -n '520,620p' crates/rustynes-mappers/src/mapper.rs

Repository: doublegate/RustyNES

Length of output: 22020


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect VRC2 construction and CHR access to determine whether it stores CHR-RAM.
rg -n -C4 'chr_is_ram|chr_rom|ppu_read|ppu_write|CHR|chr:' crates/rustynes-mappers/src/m022_vrc2.rs | sed -n '1,260p'

Repository: doublegate/RustyNES

Length of output: 6636


Persist VRC2 WRAM and CHR-RAM in the snapshot
save_state/load_state only cover bank registers and nametable VRAM. They skip prg_ram and, when chr_is_ram is set, the chr_rom buffer, so restoring a state drops cartridge save data and CHR-RAM contents. Add both buffers and bump the version tag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m022_vrc2.rs` around lines 311 - 347, The
save_state/load_state implementation must persist prg_ram and chr_rom when
chr_is_ram is enabled, in addition to the existing mapper and VRAM fields. Bump
the snapshot version tag, update the expected serialized length and offsets
consistently, and restore both buffers during load while preserving validation
and non-CHR-RAM behavior.

350-351: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicated #[cfg(test)] attribute.

The test module carries two identical #[cfg(test)] lines; the second is a stray copy-paste. Harmless to compilation but should be removed.

Proposed fix
-#[cfg(test)]
 #[cfg(test)]
 mod tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[cfg(test)]
mod tests {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m022_vrc2.rs` around lines 350 - 351, Remove the
duplicate #[cfg(test)] attribute so the test module retains exactly one
conditional-compilation attribute.
crates/rustynes-mappers/src/m033_taito_tc0190.rs (1)

59-60: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Constructor doc contradicts the actual CHR constraint. The rustdoc says "multiple of 8 KiB", but new enforces a 2 KiB multiple (CHR_BANK_2K, line 79) and the error message itself says "2 KiB multiple". Fix the doc to say 2 KiB.

Suggested change
-    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
-    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 8 KiB.
+    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
+    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 2 KiB.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 2 KiB.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m033_taito_tc0190.rs` around lines 59 - 60,
Update the constructor rustdoc for new to state that non-empty chr_rom must have
a length that is a multiple of 2 KiB, matching CHR_BANK_2K validation and the
existing error message; leave the prg_rom and CHR-RAM descriptions unchanged.
crates/rustynes-mappers/src/m069_sunsoft_fme7.rs (1)

515-534: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tail-size comments (21 / 51) contradict the actual 47-byte format.

TAIL_LEN (Line 553) evaluates to 1 + 16 + 15 + 4 + 4 + 4 + 1 + 1 + 1 = 47, and the byte-by-byte layout listed here also sums to 47 — but the prose says "21-byte tail" (Line 515) and "-- 51 bytes total --" (Line 534). The same wrong "51 bytes" figure appears at Line 926. The serialization is correct (write_tail/read_tail both key off TAIL_LEN), so this is purely a stale save-state-format annotation, but it will mislead anyone reasoning about the blob layout.

Suggested wording correction
-    /// Serialize the live audio state.  21-byte tail:
-    ///   addr_latch(1) + regs[16](16) + tone_a/b/c counter+level(3*5=15) +
-    ///   noise counter+lfsr(4+1+... wait that's bigger).
-    ///
-    /// Tail layout (kept in lock-step with `read_tail`):
+    /// Serialize the live audio state.  47-byte tail.
+    ///
+    /// Tail layout (kept in lock-step with `read_tail`):
-    ///   -- 51 bytes total --
+    ///   -- 47 bytes total (== Sunsoft5BAudio::TAIL_LEN) --
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    /// Serialize the live audio state.  47-byte tail.
    ///
    /// Tail layout (kept in lock-step with `read_tail`):
    ///   addr_latch         : 1
    ///   regs               : 16
    ///   tone_a.counter     : 4 (u32 LE)
    ///   tone_a.level       : 1
    ///   tone_b.counter     : 4
    ///   tone_b.level       : 1
    ///   tone_c.counter     : 4
    ///   tone_c.level       : 1
    ///   noise.counter      : 4
    ///   noise.lfsr         : 4 (u32 LE, only low 17 bits used)
    ///   envelope.counter   : 4
    ///   envelope.level     : 1
    ///   envelope.rising    : 1 (bool)
    ///   envelope.holding   : 1 (bool)
    ///   -- 47 bytes total (== Sunsoft5BAudio::TAIL_LEN) --
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m069_sunsoft_fme7.rs` around lines 515 - 534,
Correct the stale tail-size annotations in the live audio state serialization
documentation: change the “21-byte tail” and “51 bytes total” descriptions near
the tail layout, plus the matching “51 bytes” reference near the save-state
format documentation, to the actual 47-byte size. Do not modify the
serialization logic or TAIL_LEN.
crates/rustynes-mappers/src/m095_namcot3425.rs (3)

53-71: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale comment headers describing mappers that don't live in these files. The module split left behind block comments that document unrelated boards; they'll mislead maintainers into thinking those mappers are implemented here. Delete (or move) each orphaned block.

  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71: remove the "Mapper 28 — Action 53" block (and also the "Mapper 112 — NTDEC ASDER" block at L234-L249); this file only implements Namcot 3425 (mapper 95).
  • crates/rustynes-mappers/src/kaiser.rs#L603-L610: remove the "Waixing253 (mapper 253)" block; no such implementation exists in this file.
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47: remove the "Mapper 31" block (and the "Mapper 143 — Sachen TCA01" block at L168-L175).
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53: remove the "Mapper 38 — Bit Corp" block (and the "Mapper 241 — BxROM-like" block at L177-L183).
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53: remove the "Mapper 38" block (and the "Mapper 250 — Nitra" block at L195-L207).
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59: remove the "Mapper 28 — Action 53" block.
📍 Affects 6 files
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71 (this comment)
  • crates/rustynes-mappers/src/kaiser.rs#L603-L610
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs` around lines 53 - 71, Remove
the orphaned mapper documentation blocks without changing implementation code:
in crates/rustynes-mappers/src/m095_namcot3425.rs at lines 53-71 and 234-249
remove the Mapper 28 and Mapper 112 headers; in
crates/rustynes-mappers/src/kaiser.rs at lines 603-610 remove the Waixing253
block; in crates/rustynes-mappers/src/m107_magic_dragon107.rs at lines 40-47 and
168-175 remove the Mapper 31 and Mapper 143 blocks; in
crates/rustynes-mappers/src/m240_cne_multicart.rs at lines 47-53 and 177-183
remove the Mapper 38 and Mapper 241 blocks; in
crates/rustynes-mappers/src/m244_cne_decathlon.rs at lines 47-53 and 195-207
remove the Mapper 38 and Mapper 250 blocks; and in
crates/rustynes-mappers/src/m246_fong_shen_bang246.rs at lines 41-59 remove the
Mapper 28 block.

251-252: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicated #[cfg(test)] on the test module. A copy-paste artifact from the split — the second attribute is redundant (harmless, but confusing and possibly masking an intended #[cfg(feature = …)]). Drop the duplicate at each site.

  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252: keep one #[cfg(test)] above mod tests.
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186: keep one #[cfg(test)] above mod tests.
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210: keep one #[cfg(test)] above mod tests.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252 (this comment)
  • crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs` around lines 251 - 252,
Remove the duplicate #[cfg(test)] attribute above the test module, keeping
exactly one attribute in crates/rustynes-mappers/src/m095_namcot3425.rs lines
251-252, crates/rustynes-mappers/src/m240_cne_multicart.rs lines 185-186, and
crates/rustynes-mappers/src/m244_cne_decathlon.rs lines 209-210.

150-155: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

last - 1 underflows for a single-bank PRG, panicking on every read. last is (len/PRG_BANK_8K).max(1) - 1, so a 1-bank (8 KiB) ROM — which new accepts — gives last == 0, and read_prg(last - 1, …) at line 154 computes 0usize - 1. Since this runs unconditionally at the top of cpu_read for every address, it panics under overflow-checks regardless of the address read. ROM size is untrusted, and this module documents that "a register write can never index out of bounds … cannot afford a panic." Use a saturating subtraction (the subsequent % count in read_prg keeps the mapping correct).

Suggested fix
-            0xC000..=0xDFFF => self.read_prg(last - 1, addr),
+            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),

As per coding guidelines: outside #[cfg(test)], do not panic on untrusted data parsed from ROMs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        match addr {
            0x8000..=0x9FFF => self.read_prg(self.prg_banks[0] as usize, addr),
            0xA000..=0xBFFF => self.read_prg(self.prg_banks[1] as usize, addr),
            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),
            0xE000..=0xFFFF => self.read_prg(last, addr),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m095_namcot3425.rs` around lines 150 - 155,
Update the `cpu_read` PRG bank calculation so the `0xC000..=0xDFFF` mapping uses
saturating subtraction instead of `last - 1`, preventing underflow for
single-bank PRG ROMs. Preserve the existing `read_prg` behavior and mappings for
larger ROMs, and ensure untrusted ROM sizes cannot trigger a panic.

Source: Coding guidelines

crates/rustynes-mappers/src/m107_magic_dragon107.rs (1)

3-5: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc describes the bank decode inaccurately. The code does prg_bank = value >> 1 (PRG ignores bit 0, uses the upper bits) and chr_bank = value (CHR uses the full byte), matching the reference. The prose has it inverted — "low bit selects a 32 KiB PRG bank and the upper bits an 8 KiB CHR bank" — which will mislead the next reader.

Suggested wording
-//! One write-anywhere register in `$8000-$FFFF` carrying both bank fields:
-//! the low bit selects a 32 KiB PRG bank and the upper bits an 8 KiB CHR
-//! bank.
+//! One write-anywhere register in `$8000-$FFFF` carrying both bank fields:
+//! `value >> 1` selects the 32 KiB PRG bank and the full byte selects the
+//! 8 KiB CHR bank.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

//! One write-anywhere register in `$8000-$FFFF` carrying both bank fields:
//! `value >> 1` selects the 32 KiB PRG bank and the full byte selects the
//! 8 KiB CHR bank.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m107_magic_dragon107.rs` around lines 3 - 5,
Update the module documentation above the mapper implementation to describe the
actual decode: bit 0 is ignored for the PRG bank, which uses the upper bits for
the 32 KiB bank, while the full byte selects the 8 KiB CHR bank. Keep the
implementation unchanged.
crates/rustynes-mappers/src/m250_nitra250.rs (1)

127-137: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

last - 1 can underflow, violating the module's stated no-panic invariant.

new accepts any non-zero 8 KiB multiple, so a single-bank (8 KiB) image gives last = 0; cpu_read($C000..) with prg_mode == false then evaluates last - 1 as 0usize - 1 → overflow panic in debug builds (wrap in release). The module header (Lines 12-14) explicitly promises register access "can never index out of bounds ... cannot afford a panic". Use saturating_sub, matching Coolboy::prg_bank_mmc3 in m268_bmc_coolboy.rs.

Proposed fix
-        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
+        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
+        let second_last = last.saturating_sub(1);
         let r6 = self.bank_regs[6] as usize;
         let r7 = self.bank_regs[7] as usize;
         match (self.prg_mode, addr) {
             (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
-            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1,
+            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => second_last,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn prg_bank_for(&self, addr: u16) -> usize {
        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        let second_last = last.saturating_sub(1);
        let r6 = self.bank_regs[6] as usize;
        let r7 = self.bank_regs[7] as usize;
        match (self.prg_mode, addr) {
            (false, 0x8000..=0x9FFF) | (true, 0xC000..=0xDFFF) => r6,
            (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => second_last,
            (_, 0xA000..=0xBFFF) => r7,
            _ => last,
        }
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m250_nitra250.rs` around lines 127 - 137, Update
prg_bank_for so the fixed-bank expression using last - 1 saturates at zero,
matching the safe behavior in Coolboy::prg_bank_mmc3. Preserve the existing
bank-selection branches while ensuring single-bank PRG images cannot underflow
or panic.
crates/rustynes-mappers/src/multicart_discrete.rs (1)

1011-1015: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

32 KiB-mode PRG reads can index past the ROM for odd 16 KiB bank counts (Multicart202, Multicart212). Shared root cause: count is in 16 KiB units and lo16 is wrapped as a 16 KiB index (% count), but the fetch spans a full 32 KiB (off up to 0x7FFF). For an odd number of 16 KiB banks (e.g. 48 KiB → count == 3, page 2/3 → lo16 == count-1), lo16 * PRG_BANK_16K + off reaches len + 0x3FFF — a direct out-of-bounds index that panics in debug and release. A 48 KiB PRG passes is_multiple_of(PRG_BANK_16K), so untrusted ROMs reach it, violating the module's % count no-OOB invariant. Fix by masking the final index against prg_rom.len() (or compute in 32 KiB units as Multicart61/Multicart62 do).

  • crates/rustynes-mappers/src/multicart_discrete.rs#L1011-L1015: in Multicart202::cpu_read, index with (lo16 * PRG_BANK_16K + off) % self.prg_rom.len() (or use a 32 KiB bank count).
  • crates/rustynes-mappers/src/multicart_discrete.rs#L1380-L1383: apply the identical mask/32 KiB-count fix in Multicart212::cpu_read.
📍 Affects 1 file
  • crates/rustynes-mappers/src/multicart_discrete.rs#L1011-L1015 (this comment)
  • crates/rustynes-mappers/src/multicart_discrete.rs#L1380-L1383
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/multicart_discrete.rs` around lines 1011 - 1015,
32 KiB PRG reads can exceed ROM bounds when the 16 KiB bank count is odd. In
Multicart202::cpu_read at
crates/rustynes-mappers/src/multicart_discrete.rs#L1011-L1015 and
Multicart212::cpu_read at
crates/rustynes-mappers/src/multicart_discrete.rs#L1380-L1383, apply the same
fix by masking the computed PRG index with self.prg_rom.len(), or calculate
banks in 32 KiB units, while preserving the existing bank and offset behavior.

Source: Coding guidelines

crates/rustynes-mappers/src/ntdec.rs (1)

817-822: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

last - 1 underflows for a single-bank (8 KiB) PRG image.

When (prg_rom.len() / PRG_BANK_8K).max(1) == 1, last == 0, so last - 1 underflows and a $C000-$DFFF fetch panics under overflow-checked (debug/test) builds. An 8 KiB PRG passes the constructor's is_multiple_of(PRG_BANK_8K) check, so this is reachable from an untrusted ROM. The sibling NtdecTc112 already uses saturating_sub for exactly this case.

Proposed fix
-            0xC000..=0xDFFF => self.read_prg(last - 1, addr),
+            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
        match addr {
            0x8000..=0x9FFF => self.read_prg(self.prg_banks[0] as usize, addr),
            0xA000..=0xBFFF => self.read_prg(self.prg_banks[1] as usize, addr),
            0xC000..=0xDFFF => self.read_prg(last.saturating_sub(1), addr),
            0xE000..=0xFFFF => self.read_prg(last, addr),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/ntdec.rs` around lines 817 - 822, Update the PRG
bank calculation in the mapper read path around self.prg_banks and last so the
fixed bank for the $C000-$DFFF range cannot underflow when only one 8 KiB bank
exists. Use saturating subtraction consistent with NtdecTc112, while preserving
the existing bank mapping for images with multiple banks.

Source: Coding guidelines

crates/rustynes-ppu/src/snapshot.rs (1)

491-491: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use PPU_SNAPSHOT_VERSION instead of hardcoding 8.

The APU sibling file in this same PR fixed this exact pattern (1..=APU_SNAPSHOT_VERSION), but this line still hardcodes the literal upper bound. Every prior tail (v1→v8) required a manual edit here; missing it on the next bump will make restore reject the newest valid version with UnsupportedVersion instead of accepting it.

🔧 Proposed fix
-        if !matches!(version, 1..=8) {
+        if !matches!(version, 1..=PPU_SNAPSHOT_VERSION) {
             return Err(PpuSnapshotError::UnsupportedVersion(version));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if !matches!(version, 1..=PPU_SNAPSHOT_VERSION) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-ppu/src/snapshot.rs` at line 491, Update the version
validation in the snapshot restore logic around the matches! check to use
PPU_SNAPSHOT_VERSION as the inclusive upper bound instead of the hardcoded 8,
preserving the existing rejection of versions outside the supported range.
crates/rustynes-test-harness/tests/holy_mapperel.rs (1)

76-83: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the obsolete residual heading.

Line 76 still says M1_*/M69_* have a nonzero WRAM nibble, contradicting the new detail=0000 table and labels. Describe this as a closed/former residual instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-test-harness/tests/holy_mapperel.rs` around lines 76 - 83,
Update the residual heading in the documentation near the MMC1/FME-7 explanation
to describe M1_* and M69_* as a closed or former residual, consistent with the
detail=0000 table and labels; remove the claim that they currently have a
nonzero WRAM nibble.
crates/rustynes-test-harness/tests/snapshot_schema_audit.rs (1)

326-343: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

touches_field only checks the trailing word boundary, not the leading one.

The needle is "self.{field}", but the code only verifies the character after the match; it never checks the character before it. A local variable ending in self (e.g. myself.oam_addr, itself.oam_addr) would satisfy the check for field oam_addr even though it isn't self.oam_addr. Given the module docs describe boundary-safety as "load-bearing" and this exact audit has already caught three real regressions, the check should be symmetric.

🛡️ Proposed fix: check both boundaries
 fn touches_field(src: &str, field: &str) -> bool {
     let needle = format!("self.{field}");
     let mut from = 0;
     while let Some(hit) = src[from..].find(&needle) {
-        let after = from + hit + needle.len();
+        let abs_hit = from + hit;
+        let leading_ok = !src[..abs_hit]
+            .chars()
+            .next_back()
+            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
+        let after = abs_hit + needle.len();
         let next = src[after..].chars().next();
-        if !next.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') {
+        let trailing_ok = !next.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
+        if leading_ok && trailing_ok {
             return true;
         }
         from = after;
     }
     false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

/// Does `src` reference `self.<field>` at a word boundary?
///
/// The boundary check is load-bearing: a plain substring search would let
/// `self.oam_addr` satisfy a field named `oam`, which is exactly the kind of
/// accidental pass that makes a coverage audit worthless.
fn touches_field(src: &str, field: &str) -> bool {
    let needle = format!("self.{field}");
    let mut from = 0;
    while let Some(hit) = src[from..].find(&needle) {
        let abs_hit = from + hit;
        let leading_ok = !src[..abs_hit]
            .chars()
            .next_back()
            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
        let after = abs_hit + needle.len();
        let next = src[after..].chars().next();
        let trailing_ok = !next.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
        if leading_ok && trailing_ok {
            return true;
        }
        from = after;
    }
    false
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-test-harness/tests/snapshot_schema_audit.rs` around lines 326
- 343, Update touches_field to validate the character immediately before each
matched “self.<field>” needle as well as the existing trailing boundary,
rejecting matches embedded in identifiers such as “myself”. Preserve the current
search loop and return true only when both boundaries are valid.
docs/expansion-audio.md (1)

19-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the Mapper::mix_audio contract documentation update. These pages now describe the widened i32 behavior but retain an obsolete i16 signature, and docs/expansion-audio.md still calls the resolved 5B level deferred.

  • docs/expansion-audio.md#L19-L19: update the introductory hook signature and replace the deferred-5B paragraph with the v2.2.3 calibrated-oracle status.
  • docs/apu-2a03.md#L329-L353: update the introductory hook declaration at Line 325 to -> i32.

As per path instructions, “Docs are the spec here, not a changelog.”

📍 Affects 2 files
  • docs/expansion-audio.md#L19-L19 (this comment)
  • docs/apu-2a03.md#L329-L353
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/expansion-audio.md` at line 19, Update the Mapper::mix_audio contract
documentation: in docs/expansion-audio.md lines 19-19, change the introductory
hook signature to return i32 and replace the deferred-5B paragraph with the
v2.2.3 calibrated-oracle status; in docs/apu-2a03.md lines 329-353, update the
introductory Mapper::mix_audio declaration to return i32. Treat both documents
as the current specification rather than changelog text.

Source: Path instructions

docs/mappers.md (1)

260-262: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Finish updating renamed mapper and crate references. The affected documentation still points readers to paths that do not match the renamed workspace modules.

  • docs/mappers.md#L260-L262: replace ave_nina.rs and cne240.rs with m079_ave_nina03_06.rs and m240_cne_multicart.rs.
  • tests/roms/mmc5/README.md#L32-L32: change crates/nes-test-harness/tests/m005_mmc5.rs to crates/rustynes-test-harness/tests/m005_mmc5.rs; also update the adjacent nes-mappers crate reference.

As per path instructions, “Docs are the spec here, not a changelog.”

📍 Affects 2 files
  • docs/mappers.md#L260-L262 (this comment)
  • tests/roms/mmc5/README.md#L32-L32
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mappers.md` around lines 260 - 262, Update the documentation references
for renamed mapper and test-harness modules: in docs/mappers.md lines 260-262,
replace ave_nina.rs and cne240.rs with m079_ave_nina03_06.rs and
m240_cne_multicart.rs; in tests/roms/mmc5/README.md line 32, change the
nes-test-harness path to rustynes-test-harness and update the adjacent
nes-mappers crate reference accordingly.

Source: Path instructions

scripts/agy-review.sh (1)

186-191: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does passing a secret via git -con the command line expose it in process listings on shared/self-hosted CI runners the same waydocker run --env does?

💡 Result:

Yes, passing secrets via git command-line arguments exposes them in process listings in the same fundamental way as using docker run --env does [1][2]. On most Unix-like systems, any process's full command-line arguments are stored in the /proc filesystem (specifically /proc/[PID]/cmdline) [3][2]. This information is readable by any unprivileged user on the host, unless specific hardening measures like the hidepid mount option are enabled [3]. Because process monitoring tools like ps, top, and htop pull information from /proc, any secret passed as a command-line argument to a git command will be visible in plain text to other users or processes on that same CI runner [4][3][2]. This exposure is structurally equivalent to the risk associated with docker run --env: 1. Both methods place sensitive data in locations accessible to the process list (either /proc/PID/cmdline for arguments or /proc/PID/environ for environment variables) [1][5][3]. 2. Both can be exposed through CI/CD logs if the commands are echoed or if debug logging is enabled [6][5][7]. 3. Neither method provides actual security; they are both convenience patterns that treat secrets as plain text visible to anyone with local read access to the process environment [1][6][8]. For secure handling in CI environments, avoid passing secrets as arguments or environment variables whenever possible [5][8]. Instead, use platform-specific secret management features (such as CI/CD native secret masking, secret injection as files, or dedicated mounts) that prevent the sensitive data from appearing in logs or being exposed via simple process inspection [6][9][8][7].

Citations:


🏁 Script executed:

sed -n '150,210p' scripts/agy-review.sh
printf '\n---\n'
rg -n "GH_TOKEN|self-hosted|agy runner|fetch_refspecs|http.extraheader" -S .

Repository: doublegate/RustyNES

Length of output: 5175


🏁 Script executed:

sed -n '348,420p' scripts/agy-review.sh
printf '\n---\n'
sed -n '392,420p' CHANGELOG.md

Repository: doublegate/RustyNES

Length of output: 6653


Avoid putting GH_TOKEN in the git argv
git -c "http.extraheader=AUTHORIZATION: bearer ${GH_TOKEN}" ... exposes the bearer token in the process list on the self-hosted runner while the fetch runs. Set the header via GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0, or git config --local, so the secret never appears in command-line arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agy-review.sh` around lines 186 - 191, Update the authenticated fetch
branch in scripts/agy-review.sh to stop passing GH_TOKEN through the git
command-line -c argument. Configure http.extraheader using GIT_CONFIG_COUNT,
GIT_CONFIG_KEY_0, and GIT_CONFIG_VALUE_0 environment variables for the git
fetch, ensuring the bearer token is absent from process arguments while
preserving the existing fetch_refspecs behavior.
scripts/bench_relative_check.sh (1)

125-132: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the unrounded regression percentage.

Line 128 rounds before Line 132 evaluates the limit, so a 10.004% regression is displayed as 10.00 and passes a 10% gate. Preserve full precision for the comparison and round only for logging. As per path instructions, one-off developer tooling prioritizes correctness and clear error messages.

Proposed fix
- print(f"{b/1e6:.3f} {h/1e6:.3f} {(h - b) / b * 100:+.2f}")
+ print(f"{b/1e6:.3f} {h/1e6:.3f} {(h - b) / b * 100:.17g}")
 PY
 )"
-    printf '%-32s %12s %12s %9s%%\n' "${id}" "${base_ms}" "${head_ms}" "${delta_pct}"
-    over="$(python3 -c "print('1' if ${delta_pct} > ${MAX_REGRESSION_PCT} else '0')")"
+    printf '%-32s %12s %12s %9.2f%%\n' "${id}" "${base_ms}" "${head_ms}" "${delta_pct}"
+    over="$(python3 - "${delta_pct}" "${MAX_REGRESSION_PCT}" <<'PY'
+import sys
+print("1" if float(sys.argv[1]) > float(sys.argv[2]) else "0")
+PY
+)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    read -r base_ms head_ms delta_pct <<<"$(python3 - "$base_ns" "$head_ns" <<'PY'
import sys
b, h = int(sys.argv[1]), int(sys.argv[2])
print(f"{b/1e6:.3f} {h/1e6:.3f} {(h - b) / b * 100:.17g}")
PY
)"
    printf '%-32s %12s %12s %9.2f%%\n' "${id}" "${base_ms}" "${head_ms}" "${delta_pct}"
    over="$(python3 - "${delta_pct}" "${MAX_REGRESSION_PCT}" <<'PY'
import sys
print("1" if float(sys.argv[1]) > float(sys.argv[2]) else "0")
PY
)"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench_relative_check.sh` around lines 125 - 132, Update the Python
calculation in the benchmark comparison around the base_ms/head_ms/delta_pct
assignment to retain the full-precision regression percentage for the
MAX_REGRESSION_PCT check, while separately formatting the displayed percentage
to two decimals for printf logging. Ensure the over comparison uses the
unrounded value and preserve the existing output format.

Source: Path instructions

scripts/diag/dma_loop_span_analyzer.py (1)

8-9: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicated try statements from both CSV parsers.

Each site starts another try before closing the preceding one, leaving an unterminated outer try and making the script fail to parse.

  • scripts/diag/dma_loop_span_analyzer.py#L8-L9: retain one try parsing cyc and kind, with a targeted except (ValueError, IndexError).
  • scripts/diag/dma_sweep_analyzer.py#L8-L9: retain one try parsing cyc, kind, and held, with a targeted except (ValueError, IndexError).

As per path instructions, these scripts should prioritize “correctness and clear error messages over style.”

🧰 Tools
🪛 Ruff (0.15.21)

[error] 8-8: Multiple statements on one line (colon)

(E701)


[error] 8-8: Multiple statements on one line (semicolon)

(E702)


[error] 9-9: Do not use bare except

(E722)


[error] 9-9: try-except-continue detected, consider logging the exception

(S112)


[error] 9-9: Multiple statements on one line (colon)

(E701)

📍 Affects 2 files
  • scripts/diag/dma_loop_span_analyzer.py#L8-L9 (this comment)
  • scripts/diag/dma_sweep_analyzer.py#L8-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/dma_loop_span_analyzer.py` around lines 8 - 9, Remove the
duplicated try structure in scripts/diag/dma_loop_span_analyzer.py lines 8-9,
retaining one try around cyc and kind parsing with except (ValueError,
IndexError). Apply the same correction in scripts/diag/dma_sweep_analyzer.py
lines 8-9, retaining one try around cyc, kind, and held parsing with the
targeted exception handler so both CSV parsers are valid and report malformed
input clearly.

Source: Path instructions

scripts/diag/dma_sweep_analyzer.py (1)

23-32: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve cycle-zero halt intervals.

if prev treats a valid prior cycle 0 as absent, omitting its next interval from both outputs. Use prev is not None.

Proposed fix
-    d = c-prev if prev else 0
+    d = c - prev if prev is not None else 0
@@
-    if prev: deltas[c-prev]+=1
+    if prev is not None:
+        deltas[c - prev] += 1

As per path instructions, these scripts should prioritize “correctness and clear error messages over style.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

prev=None
for c,h in halts[:40]:
    d = c - prev if prev is not None else 0
    prev=c
    print(f"  +{d:4d}  ${h:04X}")
# period distribution
deltas=collections.Counter()
prev=None
for c,h in halts:
    if prev is not None:
        deltas[c - prev]+=1
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 31-31: Loop control variable h not used within loop body

(B007)


[error] 32-32: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/dma_sweep_analyzer.py` around lines 23 - 32, Update both
halt-interval loops in the analyzer to test whether prev is initialized using an
explicit None check rather than truthiness. Preserve cycle-zero as a valid prior
cycle so its interval appears in the printed output and the deltas Counter.

Source: Path instructions

scripts/diag/ppu2002_read_context_window.py (1)

21-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark value-only divergences in the context table.

Line 23 compares only (scanline, dot), so a differing $2002 value is shown without <<<. Include val in the comparison; this otherwise hides the flag-timing divergence that scripts/diag/ppu2002_diff_traces.py reports.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 21-21: Use format specifiers instead of percent format

(UP031)


[warning] 22-22: Use format specifiers instead of percent format

(UP031)


[warning] 24-24: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_context_window.py` around lines 21 - 24, Update the
mark condition in the context-table construction to compare each side’s
scanline, dot, and value fields, including val alongside l[2] and l[3]. Preserve
the existing requirement that both l and r exist and keep the “ <<<” output for
any tuple difference.

Source: Path instructions

scripts/diag/ppu2002_read_value_histogram.py (1)

30-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply the stated timing constraints before reporting a transition window.

The predicate accepts any four adjacent trace rows with a matching mask pattern; it never verifies the documented prerender location or dot progression. This can label unrelated reads as the target sequence. Filter on the intended scanline/dot/cycle conditions before emitting a candidate.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 36-36: Do not hardcode temporary file or directory names
Context: '/tmp/RustyNES/an_out.txt'
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.15.21)

[error] 35-35: Multiple statements on one line (semicolon)

(E702)


[warning] 36-36: Use format specifiers instead of percent format

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 30 - 36, The
transition-window predicate in the histogram loop must enforce the documented
prerender scanline, dot, and cycle timing constraints before appending to out.
Update the condition around masks and the window rows to validate those fields
from rows, while preserving the existing mask-pattern checks and output format.

Source: Path instructions

scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py (1)

20-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail or explicitly report incomplete 341-dot captures.

Both comparisons skip absent dots. An empty or truncated CSV therefore reports zero mismatches without indicating that no complete comparison occurred. Report missing-dot counts (and fail for a comparison expected to cover all 341 dots).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2004_mesen_vs_rustynes_scanline128.py` around lines 20 - 23,
Update the comparison logic around the Mesen/AnswerKey1 and Mesen/RustyNES
mismatch calculations to count missing dots on each side, explicitly report
incomplete 341-dot captures, and fail when a comparison expected to cover all
341 dots is incomplete. Do not treat absent dots as a successful zero-mismatch
comparison.

Source: Path instructions

scripts/diag/ppu2007_offset_greedy_search.py (1)

5-9: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail explicitly when the harness does not produce a stress dump.

A missing binary, nonzero exit, or absent STRESS2007= line currently becomes an IndexError; the optimizer should stop with the harness stderr and a clear diagnostic instead.

Proposed fix
-    out=subprocess.run(['./target/release/scan_dma_abort',ROM,'30000'],capture_output=True,text=True,env=env).stdout
-    line=[l for l in out.splitlines() if 'STRESS2007=' in l][0].split('STRESS2007=')[1].strip()
-    data=list(bytes.fromhex(line))
+    proc = subprocess.run(
+        ["./target/release/scan_dma_abort", ROM, "30000"],
+        capture_output=True,
+        text=True,
+        env=env,
+        check=False,
+    )
+    if proc.returncode:
+        raise RuntimeError(f"scan_dma_abort failed: {proc.stderr.strip()}")
+    stress_line = next(
+        (line for line in proc.stdout.splitlines() if "STRESS2007=" in line),
+        None,
+    )
+    if stress_line is None:
+        raise RuntimeError("scan_dma_abort produced no STRESS2007 dump")
+    data = list(bytes.fromhex(stress_line.split("STRESS2007=", 1)[1].strip()))

As per path instructions, these scripts should prioritize “correctness and clear error messages over style.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

def run(offs):
    env = dict(os.environ)
    env["RUSTYNES_2007_OFFSET"] = ",".join(str(o) for o in offs)
    proc = subprocess.run(
        ["./target/release/scan_dma_abort", ROM, "30000"],
        capture_output=True,
        text=True,
        env=env,
        check=False,
    )
    if proc.returncode:
        raise RuntimeError(f"scan_dma_abort failed: {proc.stderr.strip()}")
    stress_line = next(
        (line for line in proc.stdout.splitlines() if "STRESS2007=" in line),
        None,
    )
    if stress_line is None:
        raise RuntimeError("scan_dma_abort produced no STRESS2007 dump")
    data = list(bytes.fromhex(stress_line.split("STRESS2007=", 1)[1].strip()))
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 6-6: Command coming from incoming request
Context: subprocess.run(['./target/release/scan_dma_abort',ROM,'30000'],capture_output=True,text=True,env=env)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.21)

[error] 6-6: Multiple statements on one line (semicolon)

(E702)


[error] 7-7: subprocess call: check for execution of untrusted input

(S603)


[warning] 7-7: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


[warning] 8-8: Prefer next(l for l in out.splitlines() if 'STRESS2007=' in l) over single element slice

Replace with next(l for l in out.splitlines() if 'STRESS2007=' in l)

(RUF015)


[error] 8-8: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_offset_greedy_search.py` around lines 5 - 9, Update
run() to validate the subprocess result and STRESS2007= extraction before
decoding: detect execution failures or missing stress dumps, then terminate with
a clear diagnostic that includes the harness stderr. Preserve normal parsing for
successful runs and avoid relying on an unchecked list index.

Source: Path instructions

scripts/diag/ppu2007_stress_per_index_evaluator.py (1)

10-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the invocation contract and validate the required argument.

The documented command names a nonexistent renamed script, and invoking this script without a path raises IndexError instead of printing usage.

Proposed fix
-Usage: eval2007.py <dump-file> [-v]
+Usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]
@@
-txt = open(sys.argv[1]).read()
+if len(sys.argv) < 2:
+    print("usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]", file=sys.stderr)
+    sys.exit(2)
+
+txt = open(sys.argv[1]).read()

As per path instructions, these scripts should prioritize “correctness and clear error messages over style.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]
  <dump-file>: a scan_dma_abort stdout capture containing a `STRESS2007=`
               hex line, or a file of whitespace-separated hex bytes
               (the last 341 tokens are used).
  -v: print the per-index mismatch map (index, got, want, landing Y dot).
"""
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
key = [int(x, 16) for x in open(os.path.join(HERE, "key2007.txt")).read().split()]

if len(sys.argv) < 2:
    print("usage: ppu2007_stress_per_index_evaluator.py <dump-file> [-v]", file=sys.stderr)
    sys.exit(2)

txt = open(sys.argv[1]).read()
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 19-19: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(os.path.join(HERE, "key2007.txt"))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 21-21: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(sys.argv[1])
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 20-20: Use a context manager for opening files

(SIM115)


[warning] 22-22: Use a context manager for opening files

(SIM115)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2007_stress_per_index_evaluator.py` around lines 10 - 22,
Update the usage text to name the actual evaluator script rather than the
nonexistent eval2007.py, and validate that a dump-file argument is present
before accessing sys.argv[1]. When it is missing, print the documented usage and
exit cleanly with a clear error message; preserve the existing file-processing
behavior when the argument is provided.

Source: Path instructions

scripts/mapper-promotion/batch2.py (1)

49-64: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

ines_mapper(zip_nes(src)) crashes the whole batch on a header-less zip.

zip_nes returns None when no .nes entry is found (or could raise on a corrupt archive, since it has no try/except unlike rom_bytes in the sibling scripts). ines_mapper(None) then does len(b) and raises TypeError, aborting the run instead of recording an entry in errors like the MISSING SRC / HEADER mismatch cases already do.

🐛 Proposed fix
 def zip_nes(p):
-    z=zipfile.ZipFile(p)
-    for n in z.namelist():
-        if n.lower().endswith(".nes"): return z.read(n)
-    return None
+    try:
+        z = zipfile.ZipFile(p)
+    except zipfile.BadZipFile:
+        return None
+    for n in z.namelist():
+        if n.lower().endswith(".nes"):
+            return z.read(n)
+    return None
-    hm=ines_mapper(zip_nes(src))
-    if hm!=mid: errors.append((mid,f"HEADER {hm} != {mid}",src)); continue
+    payload = zip_nes(src)
+    hm = ines_mapper(payload) if payload else None
+    if hm != mid:
+        errors.append((mid, f"HEADER {hm} != {mid}" if payload else "NO .NES IN ZIP", src))
+        continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

def zip_nes(p):
    try:
        z = zipfile.ZipFile(p)
    except zipfile.BadZipFile:
        return None
    for n in z.namelist():
        if n.lower().endswith(".nes"):
            return z.read(n)
    return None


def snake(s):
    s = re.sub(r"\.(nes|zip)$", "", s, flags=re.I)
    s = re.sub(r"\(.*?\)", "", s)
    s = re.sub(r"\[.*?\]", "", s)
    s = re.sub(r"[^A-Za-z0-9]+", "_", s).strip("_").lower()
    return re.sub(r"_+", "_", s)

blocks = []
promoted = []
errors = []
for mid, rel, dirname in ITEMS:
    src = os.path.join(BASE, rel)
    if not os.path.isfile(src):
        errors.append((mid, "MISSING SRC", src))
        continue
    payload = zip_nes(src)
    hm = ines_mapper(payload) if payload else None
    if hm != mid:
        errors.append((mid, f"HEADER {hm} != {mid}" if payload else "NO .NES IN ZIP", src))
        continue
    tgt_dir = os.path.join(EXT, dirname)
    os.makedirs(tgt_dir, exist_ok=True)
🧰 Tools
🪛 Ruff (0.15.21)

[error] 52-52: Multiple statements on one line (colon)

(E701)


[error] 55-55: Multiple statements on one line (semicolon)

(E702)


[error] 55-55: Multiple statements on one line (semicolon)

(E702)


[error] 56-56: Multiple statements on one line (semicolon)

(E702)


[error] 58-58: Multiple statements on one line (semicolon)

(E702)


[error] 58-58: Multiple statements on one line (semicolon)

(E702)


[error] 61-61: Multiple statements on one line (colon)

(E701)


[error] 61-61: Multiple statements on one line (semicolon)

(E702)


[error] 63-63: Multiple statements on one line (colon)

(E701)


[error] 63-63: Multiple statements on one line (semicolon)

(E702)


[error] 64-64: Multiple statements on one line (semicolon)

(E702)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mapper-promotion/batch2.py` around lines 49 - 64, Update the batch
processing around zip_nes and ines_mapper so missing .nes entries or corrupt
archives do not abort the loop. Catch archive-reading failures and handle a None
payload before calling ines_mapper, recording the affected item in errors with a
clear reason and continuing to the next entry.
scripts/pr-review/list_all_threads.py (2)

2-3: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== outline list_all_threads.py ==\n'
ast-grep outline scripts/pr-review/list_all_threads.py --view expanded || true
printf '\n== outline list_unresolved_threads.py ==\n'
ast-grep outline scripts/pr-review/list_unresolved_threads.py --view expanded || true

printf '\n== list_all_threads.py ==\n'
cat -n scripts/pr-review/list_all_threads.py

printf '\n== list_unresolved_threads.py ==\n'
cat -n scripts/pr-review/list_unresolved_threads.py

Repository: doublegate/RustyNES

Length of output: 3660


Validate the GraphQL response before indexing reviewThreads.

Both helpers assume data.repository.pullRequest.reviewThreads.nodes exists; a GraphQL errors payload or partial response will currently throw a traceback instead of a clear failure.

  • scripts/pr-review/list_all_threads.py#L2-L3
  • scripts/pr-review/list_unresolved_threads.py#L39-L40
📍 Affects 2 files
  • scripts/pr-review/list_all_threads.py#L2-L3 (this comment)
  • scripts/pr-review/list_unresolved_threads.py#L2-L3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_all_threads.py` around lines 2 - 3, The GraphQL
response is indexed without validating errors or missing partial data, causing
an unclear traceback. In scripts/pr-review/list_all_threads.py lines 2-3 and
scripts/pr-review/list_unresolved_threads.py lines 2-3, validate the parsed
response before accessing repository, pullRequest, reviewThreads, or nodes;
detect GraphQL errors and absent required fields, then fail with a clear error
message instead of allowing key or type errors.

Source: Path instructions


11-11: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Render review bodies safely for terminal output.

Both scripts print arbitrary GitHub comment text without escaping control characters.

  • scripts/pr-review/list_all_threads.py#L11-L11: escape or sanitize the 280-character body preview.
  • scripts/pr-review/list_unresolved_threads.py#L7-L7: escape or sanitize the 650-character body preview.
📍 Affects 2 files
  • scripts/pr-review/list_all_threads.py#L11-L11 (this comment)
  • scripts/pr-review/list_unresolved_threads.py#L7-L7
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_all_threads.py` at line 11, Sanitize arbitrary GitHub
comment text before rendering terminal previews: update the body preview in
scripts/pr-review/list_all_threads.py at lines 11-11 and
scripts/pr-review/list_unresolved_threads.py at lines 7-7 to escape or remove
control characters while preserving the existing 280- and 650-character limits.
scripts/pr-review/README.md (1)

15-22: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files scripts/pr-review/README.md scripts/pr-review
echo '--- README excerpt ---'
cat -n scripts/pr-review/README.md | sed -n '1,120p'
echo '--- thread helper files ---'
fd -a 'list_*threads.py' scripts/pr-review || true

Repository: doublegate/RustyNES

Length of output: 2215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the README example and any helper script references for pagination behavior.
rg -n "reviewThreads|pageInfo|endCursor|hasNextPage|list_all_threads|list_unresolved_threads" scripts/pr-review -S

Repository: doublegate/RustyNES

Length of output: 1169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the example query can return more than 100 items in a deterministic way
# by reading the README only; no repository code execution.
python3 - <<'PY'
from pathlib import Path
text = Path('scripts/pr-review/README.md').read_text()
start = text.find('reviewThreads(first:100)')
print('found:', start != -1)
print(text[start-120:start+260] if start != -1 else '')
PY

Repository: doublegate/RustyNES

Length of output: 550


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n scripts/pr-review/list_all_threads.py
printf '\n---\n'
cat -n scripts/pr-review/list_unresolved_threads.py

Repository: doublegate/RustyNES

Length of output: 3160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' scripts/pr-review/list_all_threads.py | cat -n
printf '\n---\n'
sed -n '1,220p' scripts/pr-review/list_unresolved_threads.py | cat -n

Repository: doublegate/RustyNES

Length of output: 3160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- scripts/pr-review/list_all_threads.py ---'
cat -n scripts/pr-review/list_all_threads.py
echo '--- scripts/pr-review/list_unresolved_threads.py ---'
cat -n scripts/pr-review/list_unresolved_threads.py

Repository: doublegate/RustyNES

Length of output: 3254


Paginate reviewThreads or state the 100-thread limit.

reviewThreads(first:100) truncates larger PRs, so the closeout list can miss unresolved threads past the first page. Add pageInfo/endCursor handling, or document the hard cap explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/README.md` around lines 15 - 22, Update the README’s
reviewThreads GraphQL example to avoid silently truncating results: either add
pagination using pageInfo/endCursor and retrieve all thread pages, or explicitly
document that first:100 imposes a hard 100-thread limit. Ensure the closeout
command’s behavior is clear for pull requests exceeding that limit.

Source: Path instructions

to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md (1)

358-360: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the mapper-path rename in both historical pointers.

  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L358-L360: replace the remaining sprintN.rs pointer at Line 364 with current board-named paths or remove it.
  • to-dos/ROADMAP.md#L335-L338: replace the adjacent mmc2_mmc4.rs pointer at Line 340 with m010_mmc4.rs.

As per path instructions, Markdown is the specification, so source pointers must remain navigable after renames.

📍 Affects 2 files
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md#L358-L360 (this comment)
  • to-dos/ROADMAP.md#L335-L338
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md` around lines 358 - 360, Update
both historical Markdown pointers to use current board-named source paths: in
to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md lines 358-360, replace or remove the
remaining sprintN.rs reference; in to-dos/ROADMAP.md lines 335-338, replace
mmc2_mmc4.rs with m010_mmc4.rs. Keep all source pointers navigable after the
mapper-path rename.

Source: Path instructions

crates/rustynes-mappers/src/m024_vrc6.rs (1)

556-561: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Perform the mix in i32, not i16. The whole expression ((p1 + p2 + saw) - 30) * VRC6_MIX_SCALE evaluates in i16 (all three operands and VRC6_MIX_SCALE are i16); only the already-narrow result is widened. It stays in range today (30349/-29370), but the trait was widened to i32 precisely to provide headroom — adding a channel or bumping VRC6_MIX_SCALE would silently overflow the intermediate. Promote the operands before multiplying.

Suggested change
-        let p1 = i16::from(self.pulse1.output());
-        let p2 = i16::from(self.pulse2.output());
-        let saw = i16::from(self.saw.output());
-        // Center at zero: subtract approx half the peak (~30), then scale by
-        // [`VRC6_MIX_SCALE`] for a hardware-accurate level vs the 2A03 pulse.
-        i32::from(((p1 + p2 + saw) - 30) * VRC6_MIX_SCALE)
+        let p1 = i32::from(self.pulse1.output());
+        let p2 = i32::from(self.pulse2.output());
+        let saw = i32::from(self.saw.output());
+        // Center at zero: subtract approx half the peak (~30), then scale by
+        // [`VRC6_MIX_SCALE`] for a hardware-accurate level vs the 2A03 pulse.
+        ((p1 + p2 + saw) - 30) * i32::from(VRC6_MIX_SCALE)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let p1 = i32::from(self.pulse1.output());
        let p2 = i32::from(self.pulse2.output());
        let saw = i32::from(self.saw.output());
        // Center at zero: subtract approx half the peak (~30), then scale by
        // [`VRC6_MIX_SCALE`] for a hardware-accurate level vs the 2A03 pulse.
        ((p1 + p2 + saw) - 30) * i32::from(VRC6_MIX_SCALE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m024_vrc6.rs` around lines 556 - 561, Update the
VRC6 mixing expression in the mapper’s output method to promote p1, p2, saw, and
VRC6_MIX_SCALE to i32 before performing the subtraction and multiplication. Keep
the existing centering and scaling behavior, but ensure no intermediate
arithmetic occurs in i16.
crates/rustynes-mappers/src/m036_txc_policeman.rs (2)

6-6: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc typo: //! glued to sentence end.

Line 6 renders ...through the TXC accumulator chip.//! in the crate docs; the trailing //! was meant to be a separate blank doc line.

Proposed fix
-//! through the TXC accumulator chip.//!
+//! through the TXC accumulator chip.
+//!
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

//! through the TXC accumulator chip.
//!
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` at line 6, Fix the
crate-level documentation near the TXC accumulator chip description by
separating the trailing `//!` onto its own blank documentation line, so the
sentence ends cleanly and the crate docs render without the marker appended.

53-65: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Delete leftover section comments describing unrelated mappers. The mechanical module split left divider blocks documenting boards that are not implemented in the enclosing file, which is misleading to future readers.

  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65: remove the "Mapper 15 — K-1029" block (and the "Mapper 39 — Subor" block at L189-196); the file is mapper 36 only.
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167: remove the "Mapper 79 — AVE NINA-03/06" block; the file is mapper 38 only.
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52: remove the "Mapper 15 — K-1029" block (and the "Mapper 61" block at L192-203); the file is mapper 39 only.
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43: remove the "Mapper 38 — Bit Corp" block (and the "Mapper 232" block at L207-216); the file is mapper 41 only.
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58: remove the "Mapper 147 — Sachen 3018" block (and the "Mapper 185" block at L215-229); the file is mapper 180 only.
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62: remove the "Mapper 147 — Sachen 3018" block (and the "Mapper 200" block at L234-245); the file is mapper 185 only.
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44: remove the "Mapper 38 — Bit Corp" block (and the "Mapper 240" block at L191-200); the file is mapper 232 only.
📍 Affects 7 files
  • crates/rustynes-mappers/src/m036_txc_policeman.rs#L53-L65 (this comment)
  • crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167
  • crates/rustynes-mappers/src/m039_subor39.rs#L40-L52
  • crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58
  • crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m036_txc_policeman.rs` around lines 53 - 65,
Remove the leftover unrelated mapper section comments from
crates/rustynes-mappers/src/m036_txc_policeman.rs lines 53-65 and 189-196,
crates/rustynes-mappers/src/m038_bitcorp38.rs lines 161-167,
crates/rustynes-mappers/src/m039_subor39.rs lines 40-52 and 192-203,
crates/rustynes-mappers/src/m041_caltron41.rs lines 37-43 and 207-216,
crates/rustynes-mappers/src/m180_nichibutsu180.rs lines 42-58 and 215-229,
crates/rustynes-mappers/src/m185_cnrom185.rs lines 46-62 and 234-245, and
crates/rustynes-mappers/src/m232_camerica_bf9096.rs lines 38-44 and 191-200;
retain only comments describing each file’s implemented mapper.
crates/rustynes-mappers/src/m076_namcot3446.rs (1)

55-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reorg left stale mapper-description comment blocks (plus jammed //! and duplicated #[cfg(test)]) in the wrong module files. The sprint→board module rename copied comment blocks that describe boards not implemented in these files, which will mislead future maintainers. One cleanup pass per file:

  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73: remove the "Mapper 28 — Action 53" block and the "Mapper 174 — NTDEC 5-in-1" block at Lines 217-225; fix the jammed //! at Line 8; drop the duplicate #[cfg(test)] at Lines 227-228.
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L47-L59: remove the "Mapper 15 — K-1029" block and the "Mapper 96 — Bandai Oeka Kids" block at Lines 211-220; fix the jammed //! at Line 7; drop the duplicate #[cfg(test)] at Lines 222-223.
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L53-L59: remove the "Mapper 38 — Bit Corp UNL-PCI556" block and the "Mapper 113 — NINA-006" block at Lines 214-224; fix the jammed //! at Line 11; drop the duplicate #[cfg(test)] at Lines 226-227.
📍 Affects 3 files
  • crates/rustynes-mappers/src/m076_namcot3446.rs#L55-L73 (this comment)
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs#L47-L59
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L53-L59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m076_namcot3446.rs` around lines 55 - 73, Clean
up stale comments and duplicated test attributes in
crates/rustynes-mappers/src/m076_namcot3446.rs (lines 55-73 and 217-228),
crates/rustynes-mappers/src/m077_irem_napoleon.rs (lines 47-59 and 211-223), and
crates/rustynes-mappers/src/m079_ave_nina03_06.rs (lines 53-59 and 214-227):
remove the unrelated mapper-description blocks, separate the jammed module
documentation comments, and remove each duplicate #[cfg(test)] attribute while
preserving the correct mapper documentation and test configuration.
crates/rustynes-mappers/src/m088_namco118.rs (1)

257-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers of nametable_address to determine whether 4-screen boards route through it.
rg -nP -C3 '\bnametable_address\s*\(' crates/rustynes-core/src crates/rustynes-ppu/src

Repository: doublegate/RustyNES

Length of output: 4941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the mapper trait, PPU bus hooks, and Namco 118 nametable logic.
files=(
  crates/rustynes-ppu/src/bus.rs
  crates/rustynes-mappers/src/m088_namco118.rs
  crates/rustynes-core/src/bus.rs
)

for f in "${files[@]}"; do
  echo "===== $f ====="
  ast-grep outline "$f" --view expanded || true
  echo
done

echo "===== Relevant slices ====="
sed -n '1,180p' crates/rustynes-ppu/src/bus.rs
echo
sed -n '1,340p' crates/rustynes-mappers/src/m088_namco118.rs
echo
sed -n '1500,1605p' crates/rustynes-core/src/bus.rs
echo
sed -n '4090,4125p' crates/rustynes-core/src/bus.rs
echo
sed -n '2535,2620p' crates/rustynes-ppu/src/ppu.rs

Repository: doublegate/RustyNES

Length of output: 49414


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow read to the relevant implementations and interfaces.
rg -n -C3 'trait .*Bus|fn nametable_address|fn write_nametable|fn peek_nametable|fn read_nametable|fn nametable_offset|fixed_4screen|4screen|FourScreen|Four Screen' \
  crates/rustynes-ppu/src/bus.rs crates/rustynes-ppu/src/ppu.rs crates/rustynes-core/src/bus.rs crates/rustynes-mappers/src/m088_namco118.rs

echo "===== Namco 118 file around nametable methods ====="
sed -n '220,290p' crates/rustynes-mappers/src/m088_namco118.rs

echo "===== PPU bus interface ====="
sed -n '1,160p' crates/rustynes-ppu/src/bus.rs

echo "===== Core bus nametable path ====="
sed -n '1520,1580p' crates/rustynes-core/src/bus.rs

echo "===== PPU read/write nametable path ====="
sed -n '2538,2620p' crates/rustynes-ppu/src/ppu.rs

Repository: doublegate/RustyNES

Length of output: 22998


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for Namco 118 nametable hooks and how 4-screen is selected.
rg -n -C4 'fn (peek_nametable|write_nametable|nametable_address|nametable_offset|fixed_4screen|mirroring|vram)' \
  crates/rustynes-mappers/src/m088_namco118.rs

echo "===== Full Namco 118 nametable section ====="
sed -n '150,290p' crates/rustynes-mappers/src/m088_namco118.rs

Repository: doublegate/RustyNES

Length of output: 5790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether Mapper 88 provides direct nametable overrides or only address mapping.
rg -n -C3 'fn (peek_nametable|write_nametable|nametable_address|nametable_offset)|fixed_4screen|vram' crates/rustynes-mappers/src/m088_namco118.rs

Repository: doublegate/RustyNES

Length of output: 3421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the adapter methods that decide whether the mapper-owned 4-screen VRAM is actually used.
sed -n '4055,4120p' crates/rustynes-core/src/bus.rs

echo "===== Mapper trait nametable defaults / contracts ====="
rg -n -C4 'nametable_fetch|nametable_write|nametable_address' crates/rustynes-mappers/src crates/rustynes-core/src crates/rustynes-ppu/src

Repository: doublegate/RustyNES

Length of output: 50375


Route Namco 118 four-screen nametables through the full 4 KiB offset. PpuBusAdapter falls back to Mapper::nametable_address for $2000-$3EFF, and Namco118 does not implement nametable_fetch / nametable_write, so the PPU never uses the mapper's 4 KiB vram. With fixed_4screen, off & 0x07FF aliases tables 2/3; return the full offset or expose that RAM via the nametable hooks instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m088_namco118.rs` around lines 257 - 260, Update
Namco118::nametable_address to preserve the full 4 KiB nametable offset instead
of masking with 0x07FF, so fixed four-screen mode maps tables 2 and 3 without
aliasing. Keep the existing conversion and fallback behavior, and ensure the
returned address comes from nametable_offset(addr) unchanged within the
supported u16 range.
crates/rustynes-mappers/src/m113_ave_nina006.rs (1)

48-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove orphaned comment blocks describing mappers not implemented in these files. The module rename split multi-mapper files into one-board-per-file modules, but each file kept banner comments for boards that now live elsewhere (or nowhere). Every listed block documents a mapper the enclosing file does not implement, which will mislead readers about the module's scope.

  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54: delete the "Mapper 38 — Bit Corp UNL-PCI556" block (this file is mapper 113).
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L208-L217: delete the "Mapper 86 — Jaleco JF-13" block.
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70: delete the "Mapper 15 — K-1029" block (this file is mapper 132).
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275: delete the "Mapper 133 — Sachen 3009" block.
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58: delete the "Mapper 40 — NTDEC 2722" block (this file is mapper 156).
  • crates/rustynes-mappers/src/m156_daou156.rs#L229-L244: delete the "Mapper 162 — Waixing FS304" block.
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59: delete the "Mapper 31 — INL" block (this file is mapper 177).
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60: delete the "Mapper 31 — INL" block (this file is mapper 179).
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L183-L193: delete the "Mapper 58 — multicart" block.
📍 Affects 5 files
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54 (this comment)
  • crates/rustynes-mappers/src/m113_ave_nina006.rs#L208-L217
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70
  • crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275
  • crates/rustynes-mappers/src/m156_daou156.rs#L42-L58
  • crates/rustynes-mappers/src/m156_daou156.rs#L229-L244
  • crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60
  • crates/rustynes-mappers/src/m179_hengedianzi.rs#L183-L193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m113_ave_nina006.rs` around lines 48 - 54, Remove
the orphaned mapper banner comments from
crates/rustynes-mappers/src/m113_ave_nina006.rs lines 48-54 and 208-217,
crates/rustynes-mappers/src/m132_txc_22211.rs lines 58-70 and 268-275,
crates/rustynes-mappers/src/m156_daou156.rs lines 42-58 and 229-244,
crates/rustynes-mappers/src/m177_hengedianzi.rs lines 52-59, and
crates/rustynes-mappers/src/m179_hengedianzi.rs lines 53-60 and 183-193. Delete
only the banners for Mappers 38, 86, 15, 133, 40, 162, 31, 31, and 58
respectively; preserve comments describing each file’s implemented mapper.
crates/rustynes-mappers/src/m136_sachen_3011.rs (1)

206-209: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Do any mapper-136 fixtures / dispatch sites construct <32 KiB PRG?
rg -nP -C3 'Sachen3011|new_m136|\b136\b' crates/rustynes-mappers/src crates/rustynes-mappers/tests

Repository: doublegate/RustyNES

Length of output: 9722


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' crates/rustynes-mappers/src/m136_sachen_3011.rs
printf '\n---\n'
sed -n '240,380p' crates/rustynes-mappers/src/m136_sachen_3011.rs
printf '\n---\n'
sed -n '1,220p' crates/rustynes-mappers/src/lib.rs | sed -n '1,120p'
printf '\n---\n'
sed -n '120,220p' crates/rustynes-mappers/src/lib.rs

Repository: doublegate/RustyNES

Length of output: 19183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
PRG_BANK_32K = 0x8000
for len_ in [0x2000, 0x4000, 0x8000, 0x10000]:
    count = (len_ // PRG_BANK_32K) or 1
    modulus = count * PRG_BANK_32K
    worst = 0x7FFF % modulus
    print(f'len={len_:`#06x`} count={count} modulus={modulus:`#06x`} worst_index={worst:`#06x`} in_bounds={worst < len_}')
PY

Repository: doublegate/RustyNES

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "fn check_prg|PRG_BANK_32K|check_prg\(" crates/rustynes-mappers/src -g '*.rs'

Repository: doublegate/RustyNES

Length of output: 25179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A8 -B8 "fn check_prg|check_prg\(" crates/rustynes-mappers/src crates/rustynes-mappers/tests -g '*.rs'

Repository: doublegate/RustyNES

Length of output: 27012


Use the actual PRG length for $8000-$FFFF reads
check_prg() only requires an 8 KiB multiple, so 8/16 KiB PRG images can still hit this branch and panic on the current 32 KiB modulus. Wrap against self.prg_rom.len() here, or reject PRG sizes below 32 KiB.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m136_sachen_3011.rs` around lines 206 - 209,
Update the `$8000-$FFFF` match arm in the mapper read logic to use
self.prg_rom.len() as the modulo bound, preventing out-of-bounds indexing for
valid 8 KiB and 16 KiB PRG images. Preserve the existing address masking and
bank-read behavior while replacing the 32 KiB-based count calculation.
crates/rustynes-mappers/src/m185_cnrom185.rs (1)

200-231: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

chr_enabled is not serialized, so a snapshot taken before the first register write does not round-trip.

chr_enabled powers on true while chr_reg_raw == 0, but load_state reconstructs it via chr_enable_for(chr_reg_raw), and chr_enable_for(0) is false under the submapper-0 heuristic. A save captured at power-on (or any pristine state) therefore restores with CHR disabled — the exact blank-boot condition the struct doc at Lines 71-74 says the explicit chr_enabled = true was added to avoid. This is observable for snapshot/run-ahead taken before the first $8000 write.

Serialize the flag directly rather than deriving it.

Proposed fix
     fn save_state(&self) -> Vec<u8> {
-        let mut out = Vec::with_capacity(4 + self.vram.len());
+        let mut out = Vec::with_capacity(5 + self.vram.len());
         out.push(SAVE_STATE_VERSION);
         out.push(self.chr_reg_raw);
         out.push(self.chr_bank);
         out.push(self.sub_mapper);
+        out.push(u8::from(self.chr_enabled));
         out.extend_from_slice(&self.vram);
         out
     }
 
     fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
-        let expected = 4 + self.vram.len();
+        let expected = 5 + self.vram.len();
         if data.len() != expected {
             return Err(MapperError::Truncated {
                 expected,
                 got: data.len(),
             });
         }
         if data[0] != SAVE_STATE_VERSION {
             return Err(MapperError::UnsupportedVersion(data[0]));
         }
         self.chr_reg_raw = data[1];
         self.chr_bank = data[2];
         self.sub_mapper = data[3] & 0x0F;
-        self.chr_enabled = self.chr_enable_for(self.chr_reg_raw);
-        self.vram.copy_from_slice(&data[4..4 + self.vram.len()]);
+        self.chr_enabled = data[4] != 0;
+        self.vram.copy_from_slice(&data[5..5 + self.vram.len()]);
         Ok(())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    fn save_state(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(5 + self.vram.len());
        out.push(SAVE_STATE_VERSION);
        out.push(self.chr_reg_raw);
        out.push(self.chr_bank);
        out.push(self.sub_mapper);
        out.push(u8::from(self.chr_enabled));
        out.extend_from_slice(&self.vram);
        out
    }

    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
        let expected = 5 + self.vram.len();
        if data.len() != expected {
            return Err(MapperError::Truncated {
                expected,
                got: data.len(),
            });
        }
        if data[0] != SAVE_STATE_VERSION {
            return Err(MapperError::UnsupportedVersion(data[0]));
        }
        self.chr_reg_raw = data[1];
        self.chr_bank = data[2];
        self.sub_mapper = data[3] & 0x0F;
        self.chr_enabled = data[4] != 0;
        self.vram.copy_from_slice(&data[5..5 + self.vram.len()]);
        Ok(())
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rustynes-mappers/src/m185_cnrom185.rs` around lines 200 - 231, Update
save_state and load_state to serialize chr_enabled explicitly, preserving its
exact value across snapshots including pristine power-on state. Add the flag to
the saved payload and adjust the expected length and VRAM offset in load_state;
restore chr_enabled from the serialized value instead of recomputing it with
chr_enable_for.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
docs/accuracy-ledger.md (1)

43-43: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the Zapper “Nature” description.

“Single-pixel per-frame framebuffer sample” contradicts the documented 3x3 photodiode aperture and opt-in beam-relative temporal hold. Use wording such as “3x3 photodiode aperture with optional beam-relative temporal hold.”

As per path instructions, docs are the implementation specification; documentation must not drift from the behavior it describes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/accuracy-ledger.md` at line 43, Update the Zapper light-timing “Nature”
description to reflect the implemented behavior: replace the single-pixel
per-frame characterization with a 3x3 photodiode aperture and optional
beam-relative temporal hold. Keep the surrounding implementation details and
test references unchanged.

Source: Path instructions

tests/roms/assorted/README.md (1)

28-30: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the provenance-section reference.

The sentence points all upstream paths to the “blargg's NES test ROMs” section, but nestest.nes is sourced from other/nestest.nes and attributed to kevtris in tests/roms/LICENSES.md. Point readers to the relevant entries generally, or explicitly mention the separate nestest entry.

As per path instructions, Markdown documentation is the repository specification and must not drift from the authoritative licensing table.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/roms/assorted/README.md` around lines 28 - 30, Update the provenance
sentence in the README to avoid attributing every upstream path to the “blargg's
NES test ROMs” section; reference the relevant entries in tests/roms/LICENSES.md
generally or explicitly identify the separate nestest entry attributed to
kevtris.

Source: Path instructions

.github/release-notes/v2.2.3.md (1)

124-138: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the verification commands match the reported results.

The block shows only cargo test --workspace --features test-roms, but also reports external_real_games and external_extended, which require --features commercial-roms and local ROM dumps. Add the separate command or explicitly label those rows as results from a gated maintainer-only run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/release-notes/v2.2.3.md around lines 124 - 138, Update the
verification block in the v2.2.3 release notes so external_real_games and
external_extended are clearly tied to a separate cargo test command using
--features commercial-roms, or explicitly label them as gated maintainer-only
results. Keep the existing test results and other verification commands
unchanged.

Source: Path instructions

to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md (1)

358-364: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the mapper coverage count with this rename.

This active coverage section still reports 168 families, while the v2.2.3 release narrative and PR objective report 172 families. Update the count or explicitly mark it historical so the mapper-gap inventory does not contradict the current status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md` around lines 358 - 364, Update the
mapper coverage count in the “Next reusable-ASIC BMC/pirate cores” entry from
168 to the current 172-family total, or explicitly label 168 as historical while
stating the current 172 count. Keep the surrounding release and target details
unchanged.

Source: Path instructions

docs/performance.md (1)

716-725: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the benchmark summary mismatch — the prose says all four workloads had p > 0.05, but nes_run_frame_nestest_fast is listed with p = 0.01. Update either the sentence or the table so the benchmark report is internally consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/performance.md` around lines 716 - 725, Update the benchmark summary
near the measured Criterion A/B results so the prose and table agree about
nes_run_frame_nestest_fast: either revise the “all four workloads p > 0.05”
statement or correct that workload’s listed p-value, while preserving the
accurate benchmark data.

Source: Path instructions

scripts/diag/ppu2002_read_value_histogram.py (2)

15-23: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid retaining the entire trace in memory.

rows retains every parsed tuple, while the later analysis only needs aggregates, filtered counts, and a four-read window. This makes memory usage O(N) and can exhaust the process on large captures; stream the input with bounded state or enforce a documented size limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 15 - 23, Update
the trace-processing flow in this script to avoid storing every parsed tuple in
rows; replace the unbounded list with streaming, bounded state that maintains
the required aggregates, filtered counts, and four-read window as lines are
processed. Preserve the existing parsing behavior while ensuring memory usage
does not grow with the total capture size.

40-51: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Path Traversal (CWE-59)

Make the output write symlink-safe. The script still writes to the fixed /tmp/RustyNES/an_out.txt path, with no private directory setup or O_NOFOLLOW/O_EXCL guard. Use a per-user runtime/cache directory and exclusive file creation to avoid local symlink clobbering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/diag/ppu2002_read_value_histogram.py` around lines 40 - 51, Update
the output-writing logic in the script’s main flow to use a per-user
runtime/cache directory, creating it with appropriate private permissions before
writing. Replace the fixed-path open with exclusive creation using O_NOFOLLOW
and O_EXCL, and write the generated output through that safely created file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rustynes-core/src/bus.rs`:
- Around line 2499-2507: The debugger peek path must use the same temporal
Zapper result as CPU reads when zapper_temporal_light is enabled. Update
peek_port to mirror the beam-relative branch used by the CPU path, including
pre-render handling, and remove const if required; otherwise explicitly document
that debug_peek_cpu intentionally excludes the temporal model.

In `@docs/accuracy-ledger.md`:
- Line 55: Update the FME-7 entry’s description to specify that the disabled
RAM-selected window preserves the current open-bus latch via
Mapper::cpu_read_unmapped, rather than guaranteeing a fixed $7F value. Retain
$7F only as the observed Holy Mapperel test result, and ensure the documented
behavior matches the implementation contract.

In `@scripts/diag/locate_first_trace_divergence.py`:
- Around line 3-4: Update the argument validation in the script’s top-level CLI
handling to require both the input trace and output path by rejecting
invocations with fewer than three argv entries. Expand the usage message to show
both required arguments, including the output path later consumed via
sys.argv[2], while preserving the existing usage-error behavior.

In `@scripts/pr-review/list_unresolved_threads.py`:
- Around line 38-40: Update main to inspect the parsed GraphQL response for a
non-empty errors array and missing data before indexing repository, pullRequest,
reviewThreads, or nodes. Surface the GraphQL error details and exit clearly on
API failures, while preserving the existing thread-processing path for valid
responses.

---

Outside diff comments:
In @.github/release-notes/v2.2.3.md:
- Around line 124-138: Update the verification block in the v2.2.3 release notes
so external_real_games and external_extended are clearly tied to a separate
cargo test command using --features commercial-roms, or explicitly label them as
gated maintainer-only results. Keep the existing test results and other
verification commands unchanged.

In `@docs/accuracy-ledger.md`:
- Line 43: Update the Zapper light-timing “Nature” description to reflect the
implemented behavior: replace the single-pixel per-frame characterization with a
3x3 photodiode aperture and optional beam-relative temporal hold. Keep the
surrounding implementation details and test references unchanged.

In `@docs/performance.md`:
- Around line 716-725: Update the benchmark summary near the measured Criterion
A/B results so the prose and table agree about nes_run_frame_nestest_fast:
either revise the “all four workloads p > 0.05” statement or correct that
workload’s listed p-value, while preserving the accurate benchmark data.

In `@scripts/diag/ppu2002_read_value_histogram.py`:
- Around line 15-23: Update the trace-processing flow in this script to avoid
storing every parsed tuple in rows; replace the unbounded list with streaming,
bounded state that maintains the required aggregates, filtered counts, and
four-read window as lines are processed. Preserve the existing parsing behavior
while ensuring memory usage does not grow with the total capture size.
- Around line 40-51: Update the output-writing logic in the script’s main flow
to use a per-user runtime/cache directory, creating it with appropriate private
permissions before writing. Replace the fixed-path open with exclusive creation
using O_NOFOLLOW and O_EXCL, and write the generated output through that safely
created file.

In `@tests/roms/assorted/README.md`:
- Around line 28-30: Update the provenance sentence in the README to avoid
attributing every upstream path to the “blargg's NES test ROMs” section;
reference the relevant entries in tests/roms/LICENSES.md generally or explicitly
identify the separate nestest entry attributed to kevtris.

In `@to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md`:
- Around line 358-364: Update the mapper coverage count in the “Next
reusable-ASIC BMC/pirate cores” entry from 168 to the current 172-family total,
or explicitly label 168 as historical while stating the current 172 count. Keep
the surrounding release and target details unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c2d8a79-e49e-46b2-a2e2-b563fc56054f

📥 Commits

Reviewing files that changed from the base of the PR and between f8974f0 and 2581ad8.

📒 Files selected for processing (59)
  • .github/release-notes/v2.2.3.md
  • AGENTS.md
  • CHANGELOG.md
  • crates/rustynes-core/src/bus.rs
  • crates/rustynes-core/src/input_device.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m032_irem_g101.rs
  • crates/rustynes-mappers/src/m033_taito_tc0190.rs
  • crates/rustynes-mappers/src/m036_txc_policeman.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs
  • crates/rustynes-mappers/src/m048_taito_tc0690.rs
  • crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs
  • crates/rustynes-mappers/src/m076_namcot3446.rs
  • crates/rustynes-mappers/src/m077_irem_napoleon.rs
  • crates/rustynes-mappers/src/m079_ave_nina03_06.rs
  • crates/rustynes-mappers/src/m093_sunsoft3r.rs
  • crates/rustynes-mappers/src/m094_un1rom.rs
  • crates/rustynes-mappers/src/m095_namcot3425.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m097_irem_tam_s1.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/m113_ave_nina006.rs
  • crates/rustynes-mappers/src/m132_txc_22211.rs
  • crates/rustynes-mappers/src/m136_sachen_3011.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m177_hengedianzi.rs
  • crates/rustynes-mappers/src/m179_hengedianzi.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m240_cne_multicart.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m244_cne_decathlon.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m250_nitra250.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-ppu/src/snapshot.rs
  • docs/accuracy-ledger.md
  • docs/performance.md
  • docs/ppu-2c02.md
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/diag/ppu2002_read_value_histogram.py
  • scripts/diag/ppu2007_stress_per_index_evaluator.py
  • scripts/pr-review/list_unresolved_threads.py
  • tests/roms/assorted/README.md
  • to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md
  • to-dos/ROADMAP.md
💤 Files with no reviewable changes (22)
  • crates/rustynes-mappers/src/m002_uxrom.rs
  • crates/rustynes-mappers/src/m039_subor39.rs
  • crates/rustynes-mappers/src/m038_bitcorp38.rs
  • crates/rustynes-mappers/src/m232_camerica_bf9096.rs
  • crates/rustynes-mappers/src/m180_nichibutsu180.rs
  • crates/rustynes-mappers/src/sachen_8259.rs
  • crates/rustynes-mappers/src/m041_caltron41.rs
  • crates/rustynes-mappers/src/m185_cnrom185.rs
  • crates/rustynes-mappers/src/m246_fong_shen_bang246.rs
  • crates/rustynes-mappers/src/m096_bandai96.rs
  • crates/rustynes-mappers/src/m156_daou156.rs
  • crates/rustynes-mappers/src/m241_bxrom241.rs
  • crates/rustynes-mappers/src/m021_vrc4.rs
  • crates/rustynes-mappers/src/m009_mmc2.rs
  • crates/rustynes-mappers/src/m022_vrc2.rs
  • crates/rustynes-mappers/src/m107_magic_dragon107.rs
  • crates/rustynes-mappers/src/jaleco_discrete.rs
  • crates/rustynes-mappers/src/waixing.rs
  • crates/rustynes-mappers/src/homebrew_boards.rs
  • crates/rustynes-mappers/src/ntdec.rs
  • crates/rustynes-mappers/src/sachen_discrete.rs
  • crates/rustynes-mappers/src/multicart_discrete.rs

Comment thread crates/rustynes-core/src/bus.rs Outdated
Comment thread docs/accuracy-ledger.md Outdated
Comment thread scripts/diag/locate_first_trace_divergence.py Outdated
Comment thread scripts/pr-review/list_unresolved_threads.py Outdated
The re-review on 2581ad8 posted four inline threads, all follow-ups on
code that commit had just changed. Each is valid and each is fixed.

  * bus.rs peek_port: with zapper_temporal_light on, read_port serves the
    beam-relative Zapper byte but peek_port still fell through to the
    overlay's frame-granular peek(), so a debugger peek could show a
    different $4016/$4017 value than the CPU receives. peek_port now
    mirrors the temporal branch; peek() is non-mutating and every callee
    (scanline / framebuffer / read_at_scanline / read_before_visible)
    takes &self, so it stays a pure read. Costs peek_port its `const`,
    which nothing relied on. Default-off, so byte-identical on the shipped
    path.

  * docs/accuracy-ledger.md FME-7 row: said the disabled window "reads
    $7F" as if that were a fixed result. cpu_read_unmapped preserves
    whatever the open-bus latch holds; $7F is the value Holy Mapperel's
    M69_* run happens to observe, not a universal constant. Reworded to
    describe open-bus preservation with $7F as the test observation.

  * scripts/diag/locate_first_trace_divergence.py: the argv guard I added
    last commit checked `< 2` but the script also reads argv[2] (the
    output path), so a one-arg invocation raised IndexError instead of the
    usage error. Now `< 3` with both args in the usage string.

  * scripts/pr-review/list_unresolved_threads.py: the rewrite indexed
    doc["data"][...] directly. `gh api graphql` exits 0 even when the
    response carries an `errors` array with null/partial data, so a bad
    token or renamed field died with a bare KeyError that hid the cause.
    It now surfaces GraphQL errors and a missing repository/pullRequest
    explicitly, before touching the thread list.

rustynes-core 169 tests pass; clippy --workspace --all-targets -D warnings
clean (the mirrored branch tripped option_if_let_else, now map_or_else);
fmt clean; both Python helpers parse and were exercised (the GraphQL guard
exits 1 with the API message; the normal path still prints threads).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/pr-review/list_unresolved_threads.py (1)

49-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the nested GraphQL payload before iterating in scripts/pr-review/list_unresolved_threads.py. or [] turns a missing reviewThreads.nodes into a false zero-count result, and direct indexing into thread["isResolved"], thread["comments"]["nodes"], and c["databaseId"] will still raise KeyError/TypeError on partial nodes. Fail closed with an explicit error when the payload is malformed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-review/list_unresolved_threads.py` around lines 49 - 69, Update
the payload handling in the unresolved-thread iteration to validate that
reviewThreads, its nodes, each thread’s isResolved and comments.nodes, and each
comment’s databaseId are present with the expected structure before use. Replace
fallback empty-list behavior and unsafe indexing with explicit failure for
malformed payloads, while preserving normal filtering and output for valid
unresolved threads.

Sources: Path instructions, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rustynes-core/src/bus.rs`:
- Around line 2534-2550: Add a regression test for the temporal-Zapper branch in
peek_port, covering both pre-render and visible scanlines. Assert debugger peeks
match the corresponding CPU read temporal-light contribution and verify the
operation does not advance or otherwise mutate Zapper state.

---

Outside diff comments:
In `@scripts/pr-review/list_unresolved_threads.py`:
- Around line 49-69: Update the payload handling in the unresolved-thread
iteration to validate that reviewThreads, its nodes, each thread’s isResolved
and comments.nodes, and each comment’s databaseId are present with the expected
structure before use. Replace fallback empty-list behavior and unsafe indexing
with explicit failure for malformed payloads, while preserving normal filtering
and output for valid unresolved threads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da0040e8-82c6-4bae-95c7-cbafa63feb6d

📥 Commits

Reviewing files that changed from the base of the PR and between 2581ad8 and d2bdccd.

📒 Files selected for processing (4)
  • crates/rustynes-core/src/bus.rs
  • docs/accuracy-ledger.md
  • scripts/diag/locate_first_trace_divergence.py
  • scripts/pr-review/list_unresolved_threads.py

Comment thread crates/rustynes-core/src/bus.rs
…e-render writeup

Addresses CodeRabbit's follow-up asking for a regression test on the
peek_port parity fix. Adds temporal_zapper_debugger_peek_matches_cpu_read,
which pins that a debugger peek of $4017 returns the same light bit the CPU
read produces when zapper_temporal_light is on. Confirmed it actually
catches the regression: stripping the temporal branch from peek_port makes
it fail (peek reports light from the frame model, read does not), and it
passes with the branch restored.

The test needs no framebuffer or scanline poke -- neither is reachable from
rustynes-core (Ppu::scanline is pub(crate) to rustynes-ppu, and
debug_set_framebuffer is debug-hooks-gated). Instead it constructs the
divergence directly: ZapperState::from_parts(.., light_seen=true) makes the
frame path (peek -> read) report light, while the temporal path at the fresh
bus's pre-render scanline reports none. The two models must agree only if
peek_port mirrors read_port.

Also corrects an error in the previous commit's characterization of the A3
pre-render handling. That commit claimed read_before_visible fixed a live
bug where "pre-render folds onto visible row 0 and reports light." That is
NOT true in this engine: Ppu::scanline() is non-negative on every region --
the pre-render line is 261 (NTSC) / 311 (PAL), never -1 -- so
u16::try_from(scanline) always succeeds and read_at_scanline already yields
no-light for pre-render (prerender - y >= HOLD for every visible aim). The
read_before_visible branch is therefore a total-conversion FALLBACK (a
correct answer for a hypothetical negative scanline, strictly better than
the row-0 fold a bare unwrap_or(0) would give), not a fix for observable
behavior. The doc comments in bus.rs (read_port), input_device.rs
(read_before_visible), and the input_device unit test are rewritten to say
this plainly; the unit test is renamed zapper_read_before_visible_reports_no_light
and now also asserts the real pre-render line (261) already reads no-light
through the normal path.

The peek_port parity fix itself (previous commit) stands unchanged -- that
was and is a real inconsistency between the debugger and the CPU.

rustynes-core 170 tests pass; clippy --workspace --all-targets -D warnings
clean; fmt clean.
@doublegate
doublegate merged commit 2db927c into main Jul 23, 2026
24 checks passed
@doublegate
doublegate deleted the perf/fast-dotloop-promote-and-expose branch July 23, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant