release: v2.2.3 "Datum" — fast dot path promoted, PGO shipped, last two mapper residuals closed#325
Conversation
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>
|
Warning Review limit reached
Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR is a large v2.2.3 "Datum" release combining a mechanical mapper-crate reorganization (sprint-numbered modules renamed to board-named ChangesMapper Crate Restructure and Audio Widening
Estimated code review effort: 5 (Critical) | ~150 minutes PPU/Core Performance Defaults
Save-State Schema (APU v4 / PPU v8)
Zapper Temporal Model
Test ROM Rename and CI/Release
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 8 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (8 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…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.
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 Blocking issuesNone found. Suggestions
Nitpicks
Automated first-pass review by |
Adjudication of the Antigravity reviewFirst, on the review itself: this is the first agy pass that actually saw this PR. The 1.
|
|
/gemini review |
There was a problem hiding this comment.
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 winConsider an end-of-input sanity check after decoding, mirroring
Ppu::restore.
Apu::restorenever verifiesr.pos == data.len()after the v4 tail is consumed, unlikePpu::restore(Line 725 incrates/rustynes-ppu/src/snapshot.rs), which rejects any blob with leftover bytes asTruncated. 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
⛔ Files ignored due to path filters (33)
Cargo.lockis excluded by!**/*.lock,!Cargo.lockcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snapis 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.ymlAGENTS.mdCHANGELOG.mdCargo.tomlREADME.mdcrates/rustynes-apu/src/snapshot.rscrates/rustynes-core/Cargo.tomlcrates/rustynes-core/benches/full_frame.rscrates/rustynes-core/benches/snapshot_restore.rscrates/rustynes-core/src/bus.rscrates/rustynes-core/src/input_device.rscrates/rustynes-core/src/nes.rscrates/rustynes-frontend/src/app.rscrates/rustynes-frontend/src/config.rscrates/rustynes-frontend/src/debugger/settings_panel.rscrates/rustynes-frontend/src/history_viewer.rscrates/rustynes-frontend/src/runahead.rscrates/rustynes-mappers/src/bmc_simple.rscrates/rustynes-mappers/src/fds.rscrates/rustynes-mappers/src/homebrew_boards.rscrates/rustynes-mappers/src/jaleco_discrete.rscrates/rustynes-mappers/src/kaiser.rscrates/rustynes-mappers/src/lib.rscrates/rustynes-mappers/src/m000_nrom.rscrates/rustynes-mappers/src/m001_mmc1.rscrates/rustynes-mappers/src/m002_uxrom.rscrates/rustynes-mappers/src/m003_cnrom.rscrates/rustynes-mappers/src/m004_mmc3.rscrates/rustynes-mappers/src/m005_mmc5.rscrates/rustynes-mappers/src/m007_axrom.rscrates/rustynes-mappers/src/m009_mmc2.rscrates/rustynes-mappers/src/m010_mmc4.rscrates/rustynes-mappers/src/m011_color_dreams.rscrates/rustynes-mappers/src/m013_cprom.rscrates/rustynes-mappers/src/m016_bandai_fcg.rscrates/rustynes-mappers/src/m018_jaleco_ss88006.rscrates/rustynes-mappers/src/m019_namco163.rscrates/rustynes-mappers/src/m021_vrc4.rscrates/rustynes-mappers/src/m022_vrc2.rscrates/rustynes-mappers/src/m024_vrc6.rscrates/rustynes-mappers/src/m032_irem_g101.rscrates/rustynes-mappers/src/m033_taito_tc0190.rscrates/rustynes-mappers/src/m034_bnrom_nina001.rscrates/rustynes-mappers/src/m035_jy_asic.rscrates/rustynes-mappers/src/m036_txc_policeman.rscrates/rustynes-mappers/src/m038_bitcorp38.rscrates/rustynes-mappers/src/m039_subor39.rscrates/rustynes-mappers/src/m041_caltron41.rscrates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rscrates/rustynes-mappers/src/m048_taito_tc0690.rscrates/rustynes-mappers/src/m050_fds_conv_smb2j.rscrates/rustynes-mappers/src/m064_rambo1.rscrates/rustynes-mappers/src/m065_irem_h3001.rscrates/rustynes-mappers/src/m066_gxrom.rscrates/rustynes-mappers/src/m067_sunsoft3.rscrates/rustynes-mappers/src/m068_sunsoft4.rscrates/rustynes-mappers/src/m069_sunsoft_fme7.rscrates/rustynes-mappers/src/m070_bandai74.rscrates/rustynes-mappers/src/m071_camerica_bf9093.rscrates/rustynes-mappers/src/m073_vrc3.rscrates/rustynes-mappers/src/m075_vrc1.rscrates/rustynes-mappers/src/m076_namcot3446.rscrates/rustynes-mappers/src/m077_irem_napoleon.rscrates/rustynes-mappers/src/m078_irem_jaleco78.rscrates/rustynes-mappers/src/m079_ave_nina03_06.rscrates/rustynes-mappers/src/m080_taito_x1_005.rscrates/rustynes-mappers/src/m082_taito_x1_017.rscrates/rustynes-mappers/src/m085_vrc7.rscrates/rustynes-mappers/src/m087_jaleco87.rscrates/rustynes-mappers/src/m088_namco118.rscrates/rustynes-mappers/src/m089_sunsoft2.rscrates/rustynes-mappers/src/m093_sunsoft3r.rscrates/rustynes-mappers/src/m094_un1rom.rscrates/rustynes-mappers/src/m095_namcot3425.rscrates/rustynes-mappers/src/m096_bandai96.rscrates/rustynes-mappers/src/m097_irem_tam_s1.rscrates/rustynes-mappers/src/m099_vs_system.rscrates/rustynes-mappers/src/m107_magic_dragon107.rscrates/rustynes-mappers/src/m113_ave_nina006.rscrates/rustynes-mappers/src/m118_txsrom.rscrates/rustynes-mappers/src/m119_tqrom.rscrates/rustynes-mappers/src/m132_txc_22211.rscrates/rustynes-mappers/src/m136_sachen_3011.rscrates/rustynes-mappers/src/m151_konami_vs.rscrates/rustynes-mappers/src/m152_bandai152.rscrates/rustynes-mappers/src/m156_daou156.rscrates/rustynes-mappers/src/m176_bmc_fk23c.rscrates/rustynes-mappers/src/m177_hengedianzi.rscrates/rustynes-mappers/src/m179_hengedianzi.rscrates/rustynes-mappers/src/m180_nichibutsu180.rscrates/rustynes-mappers/src/m184_sunsoft1.rscrates/rustynes-mappers/src/m185_cnrom185.rscrates/rustynes-mappers/src/m210_namco175.rscrates/rustynes-mappers/src/m232_camerica_bf9096.rscrates/rustynes-mappers/src/m240_cne_multicart.rscrates/rustynes-mappers/src/m241_bxrom241.rscrates/rustynes-mappers/src/m244_cne_decathlon.rscrates/rustynes-mappers/src/m246_fong_shen_bang246.rscrates/rustynes-mappers/src/m250_nitra250.rscrates/rustynes-mappers/src/m268_bmc_coolboy.rscrates/rustynes-mappers/src/m513_sachen_9602.rscrates/rustynes-mappers/src/mapper.rscrates/rustynes-mappers/src/mmc3_clones.rscrates/rustynes-mappers/src/multicart_discrete.rscrates/rustynes-mappers/src/nsf.rscrates/rustynes-mappers/src/nsf_expansion.rscrates/rustynes-mappers/src/ntdec.rscrates/rustynes-mappers/src/sachen_8259.rscrates/rustynes-mappers/src/sachen_discrete.rscrates/rustynes-mappers/src/sprint10.rscrates/rustynes-mappers/src/sprint11.rscrates/rustynes-mappers/src/sprint12.rscrates/rustynes-mappers/src/sprint13.rscrates/rustynes-mappers/src/sprint2.rscrates/rustynes-mappers/src/sprint3.rscrates/rustynes-mappers/src/sprint5.rscrates/rustynes-mappers/src/sprint6.rscrates/rustynes-mappers/src/sprint7.rscrates/rustynes-mappers/src/sprint8.rscrates/rustynes-mappers/src/sprint9.rscrates/rustynes-mappers/src/tier.rscrates/rustynes-mappers/src/unif.rscrates/rustynes-mappers/src/waixing.rscrates/rustynes-mappers/tests/corpus.rscrates/rustynes-netplay/tests/determinism.rscrates/rustynes-netplay/tests/udp_loopback.rscrates/rustynes-ppu/Cargo.tomlcrates/rustynes-ppu/src/ppu.rscrates/rustynes-ppu/src/snapshot.rscrates/rustynes-test-harness/Cargo.tomlcrates/rustynes-test-harness/src/bin/pgo_trainer.rscrates/rustynes-test-harness/tests/accuracycoin_runahead.rscrates/rustynes-test-harness/tests/audio_expansion.rscrates/rustynes-test-harness/tests/cpu_reset.rscrates/rustynes-test-harness/tests/fast_dotloop_diff.rscrates/rustynes-test-harness/tests/holy_mapperel.rscrates/rustynes-test-harness/tests/mmc3_clone_a12.rscrates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rscrates/rustynes-test-harness/tests/movie.rscrates/rustynes-test-harness/tests/ppu_sprites.rscrates/rustynes-test-harness/tests/snapshot_schema_audit.rscrates/rustynes-test-harness/tests/visual_regression.rsdocs/STATUS.mddocs/accuracy-ledger.mddocs/adr/0034-ppu-snapshot-v8-sprite-eval-state.mddocs/apu-2a03.mddocs/compatibility.mddocs/expansion-audio.mddocs/frontend.mddocs/mappers.mddocs/performance.mddocs/ppu-2c02.mddocs/testing-strategy.mdscripts/agy-review.shscripts/bench_relative_check.shscripts/diag/README.mdscripts/diag/dma_loop_span_analyzer.pyscripts/diag/dma_sweep_analyzer.pyscripts/diag/dump_trace_csv.pyscripts/diag/locate_first_trace_divergence.pyscripts/diag/ppu2002_diff_traces.pyscripts/diag/ppu2002_isolated_exact_timed_reads.pyscripts/diag/ppu2002_prerender_vbl_reads.pyscripts/diag/ppu2002_read_context_window.pyscripts/diag/ppu2002_read_value_histogram.pyscripts/diag/ppu2004_diff_vs_answerkey.pyscripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.pyscripts/diag/ppu2004_mesen_vs_rustynes_scanline128.pyscripts/diag/ppu2007_fetch_bus_dump.pyscripts/diag/ppu2007_key_alignment_shift_scan.pyscripts/diag/ppu2007_offset_greedy_search.pyscripts/diag/ppu2007_pattern_value_sets.pyscripts/diag/ppu2007_phase_offset_ranking.pyscripts/diag/ppu2007_stress_per_index_evaluator.pyscripts/mapper-promotion/README.mdscripts/mapper-promotion/batch2.pyscripts/mapper-promotion/convert_gg.pyscripts/mapper-promotion/enumerate_staged.pyscripts/mapper-promotion/gen_promotion.pyscripts/mapper-promotion/mapper_scan.pyscripts/mapper-promotion/promo_tests.rsscripts/mapper-promotion/promo_tests_2.rsscripts/mapper-promotion/scan.pyscripts/perf/perf_capture.shscripts/pr-review/README.mdscripts/pr-review/list_all_threads.pyscripts/pr-review/list_unresolved_threads.pyscripts/release-automation/split_commit.shtests/roms/LICENSES.mdtests/roms/README.mdtests/roms/assorted/README.mdtests/roms/assorted/apu_01_len_ctr.nestests/roms/assorted/apu_02_len_table.nestests/roms/assorted/branch_timing_1_basics.nestests/roms/assorted/branch_timing_2_backward.nestests/roms/assorted/branch_timing_3_forward.nestests/roms/assorted/cpu_reset_ram_after_reset.nestests/roms/assorted/cpu_reset_registers.nestests/roms/assorted/cpu_timing_test.nestests/roms/assorted/flowing_palette.nestests/roms/assorted/full_palette.nestests/roms/assorted/nestest.nestests/roms/assorted/oam_read.nestests/roms/assorted/oam_stress.nestests/roms/mmc5/README.mdto-dos/DEFERRED-AND-CARRYOVER-FEATURES.mdto-dos/ROADMAP.md
There was a problem hiding this comment.
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)], whileCHANGELOG.mdlines 355-360 documentdefault_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-romsverification. 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
-1to visible scanline0.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-L54crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315crates/rustynes-mappers/src/m156_daou156.rs#L42-L58crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60crates/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 & 0x7FFFthen reaches0x7FFCon the reset-vector fetch ($FFFC), indexing past a slice that is only0x2000/0x4000/0x6000bytes 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_prgfor this board to reject non-32 KiB-multiple PRG.PRG_BANK_32Kwould 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 implementsCnRom185/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 implementsCamerica232/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 implementsCne240/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 implementsBxrom241/241).crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53: remove the "Mapper 38" banner (file implementsDecathlon244/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 implementsFongShenBang246/246).crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58: remove the "Mapper 40 — NTDEC 2722" banner (file implementsNitra250/250).crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451: remove the "Sachen9602 (mapper 513)" banner (that board lives inm513_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-L245crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L38-L44crates/rustynes-mappers/src/m232_camerica_bf9096.rs#L191-L200crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451crates/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 abovemod tests.crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210: remove one of the two#[cfg(test)]lines abovemod 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 - 1underflows for a single-bank PRG image.
lastis(len / 8K).max(1) - 1, i.e.0for an 8 KiB PRG. The(false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1arm then evaluates0usize - 1: this panics in a debug build (overflow check) and wraps tousize::MAXin release (masked later by% countinread_prg), so the two build profiles diverge silently.newaccepts any non-zero multiple of 8 KiB, so a crafted ROM header reaches this on a register-driven read. Usesaturating_sub, matchingm004_mmc3/m268_bmc_coolboy.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".Proposed fix
- (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1, + (false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last.saturating_sub(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.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_*.rsfiles, 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)" precedesMulticart61(mapper 61); retitle to mapper 61 (further instances at L595-L606 "Mapper 72" aboveMulticart200, and L2223-L2232 "Mapper 111" aboveMaxi15M234).crates/rustynes-mappers/src/ntdec.rs#L246-L260: "Mapper 76 — NAMCOT-3446" precedesNtdec174(mapper 174); retitle (also L60-L78 "Mapper 28 Action 53" aboveNtdec63, L411-L422 "Mapper 225" aboveNtdec2722M40, L727-L741 "Mapper 95" aboveNtdecAsder112, L1123-L1131 "Bmc204" aboveNtdecN625092).crates/rustynes-mappers/src/sachen_8259.rs#L58-L74: "Mapper 40 — NTDEC 2722" precedesSachen8259M137(mapper 137); retitle (also L248-L261 "Mapper 156" aboveSachen8259Variant).crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74: "Mapper 15 — K-1029" precedesSachen133(mapper 133); retitle (also L1273-L1282 "Mapper 180" aboveSachenTca01M143).crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedesWaixing242(mapper 242); retitle (also L270-L284 "Mapper 246" aboveWaixingFs304M162, L647-L656 "Mapper 244" aboveWaixing253).📍 Affects 5 files
crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301(this comment)crates/rustynes-mappers/src/ntdec.rs#L246-L260crates/rustynes-mappers/src/sachen_8259.rs#L58-L74crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74crates/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 -40Repository: 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.rsRepository: 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.rsRepository: doublegate/RustyNES
Length of output: 12900
Guard these PRG reads with board-specific minimum sizes.
crates/rustynes-mappers/src/multicart_discrete.rs#L3316-L3320andcrates/rustynes-mappers/src/bmc_simple.rs#L265-L269:check_prgonly 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_32Kor>= 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/srcRepository: doublegate/RustyNES
Length of output: 529
vblank_io_torture_romstill doesn’t structurally cover idle-line$2006/$2007staging
COPY_V_DELAYis only 4 dots and the$2007read 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$2001idle-line arm, but it does not clearly force$2006/$2007onto 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
i32mixer contract.This PR widens
Mapper::mix_audiotoi32and calibrates/asserts Sunsoft 5B, but both documents retain the oldi16contract.
docs/expansion-audio.md#L19-L19: update the hook signature and replace the deferred-5B-level narrative with the v2.2.3 calibratedlevel_db_5bstatus.docs/apu-2a03.md#L329-L332: update the expansion-audio introduction immediately above this table fromi16toi32.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.rsandcne240.rsdo not match this PR’s board-named modules. Listm079_ave_nina03_06.rs,m113_ave_nina006.rs, andm240_cne_multicart.rsinstead.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; usev2.2.3or 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
.rnsstates are rejected after thePPU_SNAPSHOT_VERSIONbump. 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.ymlRepository: 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.ymlRepository: doublegate/RustyNES
Length of output: 5014
Skip an explicitly empty baseline ref.
scripts/bench_relative_check.sh:49turns an emptyBENCH_BASE_REFintoHEAD~1, so the relative bench can run against the parent commit instead of skipping onworkflow_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
exceptfollowed bycontinue, so malformed rows disappear from the computed statistics without warning.
scripts/diag/dma_loop_span_analyzer.py#L5-L11: catchValueErrorexplicitly 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-continuedetected, 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
Nonecheck for the previous cycle.If the first valid halt occurs at cycle
0, bothif prevchecks 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
hnot 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. Useargparseor an explicit usage check before indexingsys.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.
markignoresval, so equal(scanline, dot)rows with different$2002values appear unmarked. Includel[4]andr[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
/tmpinput 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)raisesStopIterationfor an empty file, whileint(...)raises an uncontextualizedValueErrorfor 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
dotsorsls, 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 full0..=340coverage 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 ofdot2val.get(d, None)Replace
dot2val.get(d, None)withdot2val.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-L48scripts/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, butrange(0, 40)excludes dot40. Either change the banner to0..39or iterate throughrange(0, 41).🧰 Tools
🪛 Ruff (0.15.21)
[warning] 43-43: Unnecessary
startargument inrangeRemove
startargument(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_abortbefore parsing its output.A failed subprocess or output without
STRESS2007=makes[...][0]raiseIndexError; a short payload then fails later duringrange(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:
subprocesscall: check for execution of untrusted input(S603)
[warning] 7-7:
subprocess.runwithout explicitcheckargumentAdd explicit
check=False(PLW1510)
[warning] 8-8: Prefer
next(l for l in out.splitlines() if 'STRESS2007=' in l)over single element sliceReplace 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 concatenationReplace 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; thereforeval(d, -1)returnsNone(idx == -2). Formatting that value with:02XraisesTypeError, 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 returnNone, crashingines_mapperinstead of recording an error.When a staged
.zipcontains no.nesentry,zip_nesreturnsNone, andines_mapper(None)hitslen(b)→TypeError, aborting the whole batch. That defeats the per-item error collection the loop is built around (contrast theMISSING SRCpath at Line 61).As per path instructions: "Prioritize correctness and clear error messages over style".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)📝 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
.rsoutput is written to a session-specific/tmp/claude-.../scratchpad/path that won't exist on re-run. Both scripts hardcode a nested/tmpdirectory that is never created;open(..., "w")will raiseFileNotFoundErroron the missing parent after the script has already done all its scanning/copying work. Write next to the script (e.g.BASE/CWD) or atempfile.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 forpromo_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](androms[0]) raise opaqueKeyError/IndexErrorwhen a promotion id lacks a staged dir/ROM.
enumerate_staged.pyhandles the same situation gracefully withdirs.get(mid)and amissinglist; this script hard-indexes instead, so a single un-stagedPROMOTEid aborts generation with a stack trace rather than a readable diagnostic.As per path instructions: "Prioritize correctness and clear error messages over style".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📝 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.nodesmay be empty, and GitHub can return a nullauthorfor deleted or unavailable users. The current indexing andc["author"]["login"]dereference abort the unresolved-thread listing. Use the same guarded extraction pattern aslist_all_threads.py, withc.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
reviewThreadsbefore 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 describinglist_all_threads.pyas 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 -SRepository: 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 addonly records the newm119_tqrom.rsandm004_mmc3.rsfiles here; afterreset --softandrestore --staged ., the oldtqrom.rsandmmc3.rsentries 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/...andrustynes-mappers; the retainednes-*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 remainingsprintN.rsreference with the current board-numbered module layout.to-dos/ROADMAP.md#L340-L340: replacemmc2_mmc4.rswithm010_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 == truethe fixed bank serves$8000-$BFFFwhile$C000-$FFFF(containing the$FFFA-$FFFFvectors) is the switchable window — asm92_uses_5bit_prg_fieldconfirms ($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 theKaiserBoardenum (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-L200crates/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.rsRepository: 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.rsRepository: 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 120Repository: 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 -nRepository: 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_statecover only mapper registers +vram, so$6000-$7FFFcontents are lost on round-trip.crates/rustynes-mappers/src/m021_vrc4.rs: same omission; IRQ state round-trips, butprg_ramdoes not.crates/rustynes-mappers/src/m022_vrc2.rs: same omission.Append
prg_ramto 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-L458crates/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 - 2underflows on an 8 KiB PRG image in both boards. Eachread_prgderivesbank_countfrom a ROM-supplied PRG size thatnew()permits to be a single 8 KiB bank, then subtracts 2 for the hardwired{-2}slot;1usize - 2panics under debug/overflow-checked builds (the trailing% bank_countonly saves release). Sibling boardsm076/m088already guard this withsaturating_sub.
crates/rustynes-mappers/src/m032_irem_g101.rs#L125-L148: replace bothbank_count - 2arms (mode-1$8000fixed and mode-0$C000fixed) withbank_count.saturating_sub(2).crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117: replace the slot-2bank_count - 2arm withbank_count.saturating_sub(2).As per coding guidelines: "do not add ...
panic!()... applied to untrusted data parsed from ROMs ... Return a typedResultor 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-L5crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7crates/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 implementsTxc36, 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-L196crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167crates/rustynes-mappers/src/m039_subor39.rs#L40-L52crates/rustynes-mappers/src/m039_subor39.rs#L192-L203crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43crates/rustynes-mappers/src/m041_caltron41.rs#L207-L216crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71crates/rustynes-mappers/src/m095_namcot3425.rs#L234-L249crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65crates/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-L185crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252crates/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_LENis 47. The enumerated field list sums to1 + 16 + 3*5 + 4 + 4 + 4 + 1 + 1 + 1 = 47, matching theTAIL_LENconstant andwrite_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 theirsave_state/load_stateserialize only register scalars,prg_ram, andvram. Because the load-sideexpectedlength also omits CHR, the round-trip silently discards CHR-RAM instead of erroring. Sibling mappersm068/m070/m071already serializechrwhenchr_is_ram.
crates/rustynes-mappers/src/m069_sunsoft_fme7.rs#L922-L946: serializechr_romwhenchr_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 extendload_state'score_expectedaccordingly.crates/rustynes-mappers/src/m075_vrc1.rs#L179-L219: appendchr_romaftervramwhenchr_is_raminsave_state, and add the matching read pluschr_ram_lenterm toexpectedinload_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/srcRepository: 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 200Repository: 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.rsRepository: doublegate/RustyNES
Length of output: 20750
🏁 Script executed:
#!/bin/bash set -euo pipefail wc -l crates/rustynes-mappers/src/m085_vrc7.rsRepository: 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.rsRepository: doublegate/RustyNES
Length of output: 12624
Persist VRC7 WRAM in the snapshot blob
save_state/load_stateomitprg_ram, and the core snapshot path only storesMapper::save_state()for MAP. That drops the 8 KiB$6000-$7FFFRAM across save/restore and rewind; append it here, bump the version, and add a round-trip assertion for a$6000write.🤖 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_bankcan never reach the additional banks; the modulo inppu_readtherefore silently renders only the first four banks. Reject sizes above4 * CHR_BANK_8Kor 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 ifself.vramwere 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 - 1underflows for a single-bank (8 KiB) PRG image.
last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1, solast == 0for a 1-bank ROM; the$C000-$DFFFarm then computeslast - 1, ausizeunderflow. 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
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (33)
Cargo.lockis excluded by!**/*.lock,!Cargo.lockcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snapis 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.gitignoreAGENTS.mdCHANGELOG.mdCargo.tomlREADME.mdcrates/rustynes-apu/src/snapshot.rscrates/rustynes-core/Cargo.tomlcrates/rustynes-core/benches/full_frame.rscrates/rustynes-core/benches/snapshot_restore.rscrates/rustynes-core/src/bus.rscrates/rustynes-core/src/input_device.rscrates/rustynes-core/src/nes.rscrates/rustynes-frontend/src/app.rscrates/rustynes-frontend/src/config.rscrates/rustynes-frontend/src/debugger/settings_panel.rscrates/rustynes-frontend/src/history_viewer.rscrates/rustynes-frontend/src/runahead.rscrates/rustynes-mappers/src/bmc_simple.rscrates/rustynes-mappers/src/fds.rscrates/rustynes-mappers/src/homebrew_boards.rscrates/rustynes-mappers/src/jaleco_discrete.rscrates/rustynes-mappers/src/kaiser.rscrates/rustynes-mappers/src/lib.rscrates/rustynes-mappers/src/m000_nrom.rscrates/rustynes-mappers/src/m001_mmc1.rscrates/rustynes-mappers/src/m002_uxrom.rscrates/rustynes-mappers/src/m003_cnrom.rscrates/rustynes-mappers/src/m004_mmc3.rscrates/rustynes-mappers/src/m005_mmc5.rscrates/rustynes-mappers/src/m007_axrom.rscrates/rustynes-mappers/src/m009_mmc2.rscrates/rustynes-mappers/src/m010_mmc4.rscrates/rustynes-mappers/src/m011_color_dreams.rscrates/rustynes-mappers/src/m013_cprom.rscrates/rustynes-mappers/src/m016_bandai_fcg.rscrates/rustynes-mappers/src/m018_jaleco_ss88006.rscrates/rustynes-mappers/src/m019_namco163.rscrates/rustynes-mappers/src/m021_vrc4.rscrates/rustynes-mappers/src/m022_vrc2.rscrates/rustynes-mappers/src/m024_vrc6.rscrates/rustynes-mappers/src/m032_irem_g101.rscrates/rustynes-mappers/src/m033_taito_tc0190.rscrates/rustynes-mappers/src/m034_bnrom_nina001.rscrates/rustynes-mappers/src/m035_jy_asic.rscrates/rustynes-mappers/src/m036_txc_policeman.rscrates/rustynes-mappers/src/m038_bitcorp38.rscrates/rustynes-mappers/src/m039_subor39.rscrates/rustynes-mappers/src/m041_caltron41.rscrates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rscrates/rustynes-mappers/src/m048_taito_tc0690.rscrates/rustynes-mappers/src/m050_fds_conv_smb2j.rscrates/rustynes-mappers/src/m064_rambo1.rscrates/rustynes-mappers/src/m065_irem_h3001.rscrates/rustynes-mappers/src/m066_gxrom.rscrates/rustynes-mappers/src/m067_sunsoft3.rscrates/rustynes-mappers/src/m068_sunsoft4.rscrates/rustynes-mappers/src/m069_sunsoft_fme7.rscrates/rustynes-mappers/src/m070_bandai74.rscrates/rustynes-mappers/src/m071_camerica_bf9093.rscrates/rustynes-mappers/src/m073_vrc3.rscrates/rustynes-mappers/src/m075_vrc1.rscrates/rustynes-mappers/src/m076_namcot3446.rscrates/rustynes-mappers/src/m077_irem_napoleon.rscrates/rustynes-mappers/src/m078_irem_jaleco78.rscrates/rustynes-mappers/src/m079_ave_nina03_06.rscrates/rustynes-mappers/src/m080_taito_x1_005.rscrates/rustynes-mappers/src/m082_taito_x1_017.rscrates/rustynes-mappers/src/m085_vrc7.rscrates/rustynes-mappers/src/m087_jaleco87.rscrates/rustynes-mappers/src/m088_namco118.rscrates/rustynes-mappers/src/m089_sunsoft2.rscrates/rustynes-mappers/src/m093_sunsoft3r.rscrates/rustynes-mappers/src/m094_un1rom.rscrates/rustynes-mappers/src/m095_namcot3425.rscrates/rustynes-mappers/src/m096_bandai96.rscrates/rustynes-mappers/src/m097_irem_tam_s1.rscrates/rustynes-mappers/src/m099_vs_system.rscrates/rustynes-mappers/src/m107_magic_dragon107.rscrates/rustynes-mappers/src/m113_ave_nina006.rscrates/rustynes-mappers/src/m118_txsrom.rscrates/rustynes-mappers/src/m119_tqrom.rscrates/rustynes-mappers/src/m132_txc_22211.rscrates/rustynes-mappers/src/m136_sachen_3011.rscrates/rustynes-mappers/src/m151_konami_vs.rscrates/rustynes-mappers/src/m152_bandai152.rscrates/rustynes-mappers/src/m156_daou156.rscrates/rustynes-mappers/src/m176_bmc_fk23c.rscrates/rustynes-mappers/src/m177_hengedianzi.rscrates/rustynes-mappers/src/m179_hengedianzi.rscrates/rustynes-mappers/src/m180_nichibutsu180.rscrates/rustynes-mappers/src/m184_sunsoft1.rscrates/rustynes-mappers/src/m185_cnrom185.rscrates/rustynes-mappers/src/m210_namco175.rscrates/rustynes-mappers/src/m232_camerica_bf9096.rscrates/rustynes-mappers/src/m240_cne_multicart.rscrates/rustynes-mappers/src/m241_bxrom241.rscrates/rustynes-mappers/src/m244_cne_decathlon.rscrates/rustynes-mappers/src/m246_fong_shen_bang246.rscrates/rustynes-mappers/src/m250_nitra250.rscrates/rustynes-mappers/src/m268_bmc_coolboy.rscrates/rustynes-mappers/src/m513_sachen_9602.rscrates/rustynes-mappers/src/mapper.rscrates/rustynes-mappers/src/mmc3_clones.rscrates/rustynes-mappers/src/multicart_discrete.rscrates/rustynes-mappers/src/nsf.rscrates/rustynes-mappers/src/nsf_expansion.rscrates/rustynes-mappers/src/ntdec.rscrates/rustynes-mappers/src/sachen_8259.rscrates/rustynes-mappers/src/sachen_discrete.rscrates/rustynes-mappers/src/sprint10.rscrates/rustynes-mappers/src/sprint11.rscrates/rustynes-mappers/src/sprint12.rscrates/rustynes-mappers/src/sprint13.rscrates/rustynes-mappers/src/sprint2.rscrates/rustynes-mappers/src/sprint3.rscrates/rustynes-mappers/src/sprint5.rscrates/rustynes-mappers/src/sprint6.rscrates/rustynes-mappers/src/sprint7.rscrates/rustynes-mappers/src/sprint8.rscrates/rustynes-mappers/src/sprint9.rscrates/rustynes-mappers/src/tier.rscrates/rustynes-mappers/src/unif.rscrates/rustynes-mappers/src/waixing.rscrates/rustynes-mappers/tests/corpus.rscrates/rustynes-netplay/tests/determinism.rscrates/rustynes-netplay/tests/udp_loopback.rscrates/rustynes-ppu/Cargo.tomlcrates/rustynes-ppu/src/ppu.rscrates/rustynes-ppu/src/snapshot.rscrates/rustynes-test-harness/Cargo.tomlcrates/rustynes-test-harness/src/bin/pgo_trainer.rscrates/rustynes-test-harness/tests/accuracycoin_runahead.rscrates/rustynes-test-harness/tests/audio_expansion.rscrates/rustynes-test-harness/tests/cpu_reset.rscrates/rustynes-test-harness/tests/fast_dotloop_diff.rscrates/rustynes-test-harness/tests/holy_mapperel.rscrates/rustynes-test-harness/tests/mmc3_clone_a12.rscrates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rscrates/rustynes-test-harness/tests/movie.rscrates/rustynes-test-harness/tests/ppu_sprites.rscrates/rustynes-test-harness/tests/snapshot_schema_audit.rscrates/rustynes-test-harness/tests/visual_regression.rsdocs/STATUS.mddocs/accuracy-ledger.mddocs/adr/0034-ppu-snapshot-v8-sprite-eval-state.mddocs/apu-2a03.mddocs/compatibility.mddocs/expansion-audio.mddocs/frontend.mddocs/mappers.mddocs/performance.mddocs/ppu-2c02.mddocs/testing-strategy.mdscripts/agy-review.shscripts/bench_relative_check.shscripts/diag/README.mdscripts/diag/dma_loop_span_analyzer.pyscripts/diag/dma_sweep_analyzer.pyscripts/diag/dump_trace_csv.pyscripts/diag/locate_first_trace_divergence.pyscripts/diag/ppu2002_diff_traces.pyscripts/diag/ppu2002_isolated_exact_timed_reads.pyscripts/diag/ppu2002_prerender_vbl_reads.pyscripts/diag/ppu2002_read_context_window.pyscripts/diag/ppu2002_read_value_histogram.pyscripts/diag/ppu2004_diff_vs_answerkey.pyscripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.pyscripts/diag/ppu2004_mesen_vs_rustynes_scanline128.pyscripts/diag/ppu2007_fetch_bus_dump.pyscripts/diag/ppu2007_key_alignment_shift_scan.pyscripts/diag/ppu2007_offset_greedy_search.pyscripts/diag/ppu2007_pattern_value_sets.pyscripts/diag/ppu2007_phase_offset_ranking.pyscripts/diag/ppu2007_stress_per_index_evaluator.pyscripts/mapper-promotion/README.mdscripts/mapper-promotion/batch2.pyscripts/mapper-promotion/convert_gg.pyscripts/mapper-promotion/enumerate_staged.pyscripts/mapper-promotion/gen_promotion.pyscripts/mapper-promotion/mapper_scan.pyscripts/mapper-promotion/promo_tests.rsscripts/mapper-promotion/promo_tests_2.rsscripts/mapper-promotion/scan.pyscripts/perf/perf_capture.shscripts/pr-review/README.mdscripts/pr-review/list_all_threads.pyscripts/pr-review/list_unresolved_threads.pyscripts/release-automation/split_commit.shtests/roms/LICENSES.mdtests/roms/README.mdtests/roms/assorted/README.mdtests/roms/assorted/apu_01_len_ctr.nestests/roms/assorted/apu_02_len_table.nestests/roms/assorted/branch_timing_1_basics.nestests/roms/assorted/branch_timing_2_backward.nestests/roms/assorted/branch_timing_3_forward.nestests/roms/assorted/cpu_reset_ram_after_reset.nestests/roms/assorted/cpu_reset_registers.nestests/roms/assorted/cpu_timing_test.nestests/roms/assorted/flowing_palette.nestests/roms/assorted/full_palette.nestests/roms/assorted/nestest.nestests/roms/assorted/oam_read.nestests/roms/assorted/oam_stress.nestests/roms/mmc5/README.mdto-dos/DEFERRED-AND-CARRYOVER-FEATURES.mdto-dos/ROADMAP.md
There was a problem hiding this comment.
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 defaultbooltofalseand silently disable the fast path for older configs. Update this release note to matchcrates/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
.rnsfiles are rejected,.rnmplayback 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/workflowsRepository: 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:
- 1: https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations
- 2: https://docs.github.com/en/enterprise-server@3.13/actions/reference/reusable-workflows-reference
- 3: https://fixdevs.com/blog/github-actions-reusable-workflow/
- 4: https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows
Security Misconfiguration (CWE-250)
Reachability: External
Add an explicit read-only permissions block here
Reusable workflows do not inherit the caller job’sGITHUB_TOKENscopes; this workflow uses its own permissions or the repository default. Sincepgoruns build scripts, benchmarks, and tests, setpermissions: { contents: read }on the workflow or thepgojob 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.mdreports 2219 passed for the whole--features test-romssuite, while.github/release-notes/v2.2.3.mdreports 2237 forcargo 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_deviceand aportpanic, but are attached toset_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-1on the pre-render line, butunwrap_or(0)turns it into scanline 0. A Zapper aimed aty == 0can 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 thetestsmodule. 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-L462crates/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 - 2underflows on a single-bank PRG-ROM. The constructor accepts any non-zero 8 KiB multiple, sobank_count == 1is reachable from a crafted iNES image. In default mode 0 the first$C000-$DFFFread evaluatesbank_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. Usesaturating_sub(2)to match the fixed-tail idiom already inm088_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: splitchip.//!intochip.then a blank//!line.crates/rustynes-mappers/src/m095_namcot3425.rs#L7-L7: splitregister.//!intoregister.then a blank//!line.crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L6-L6: splitload-bearing.//!intoload-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-L7crates/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-L167crates/rustynes-mappers/src/m039_subor39.rs#L40-L52crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56crates/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 thetestsmodule. 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)]abovemod tests.crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227: remove the duplicate#[cfg(test)]abovemod tests.crates/rustynes-mappers/src/m094_un1rom.rs#L184-L185: remove the duplicate#[cfg(test)]abovemod 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-L227crates/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 || trueRepository: 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 doneRepository: doublegate/RustyNES
Length of output: 2111
Persist the live VRC7 OPLL state in snapshots
crates/rustynes-mappers/src/m085_vrc7.rs:535-632
save_stateonly writes the shadow register bytes here;load_statenever restores the liveself.opllstate 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 -nRepository: 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:
- 1: https://www.nesdev.org/wiki/INES_Mapper_093
- 2: https://github.com/mamedev/mame/blob/453b8e7d/src/devices/bus/nes/sunsoft.cpp
- 3: http://gamebank.jp/dumper/dl/93.nut
- 4: https://www.nesdev.org/wiki/Sunsoft_2_pinout
- 5: https://forums.nesdev.org/viewtopic.php?t=6585
- 6: https://forums.nesdev.org/viewtopic.php?t=12545
🌐 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:
- 1: https://www.nesdev.org/wiki/INES_Mapper_093
- 2: https://github.com/mamedev/mame/blob/453b8e7d/src/devices/bus/nes/sunsoft.cpp
- 3: http://gamebank.jp/dumper/dl/93.nut
- 4: https://www.nesdev.org/wiki/Sunsoft_2_pinout
- 5: https://forums.nesdev.org/viewtopic.php?t=6585
- 6: https://mamedev.emulab.it/etabeta/2009/09/30/nessundorma/
🏁 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' doneRepository: doublegate/RustyNES
Length of output: 2111
Apply the bus-conflict mask before decoding this register. Mapper 93 is bus-conflict affected, so
valueshould be ANDed withself.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
m107file 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-L175crates/rustynes-mappers/src/m113_ave_nina006.rs#L48-L54crates/rustynes-mappers/src/m113_ave_nina006.rs#L208-L217crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275crates/rustynes-mappers/src/m136_sachen_3011.rs#L306-L315crates/rustynes-mappers/src/m156_daou156.rs#L42-L58crates/rustynes-mappers/src/m156_daou156.rs#L229-L244crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59crates/rustynes-mappers/src/m177_hengedianzi.rs#L182-L188crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60crates/rustynes-mappers/src/m179_hengedianzi.rs#L183-L193crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58crates/rustynes-mappers/src/m180_nichibutsu180.rs#L215-L229crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62crates/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: splitwindow.//!intowindow.+ a blank//!line.crates/rustynes-mappers/src/m136_sachen_3011.rs#L11-L11: splitm136_sachen_3011.rs.//!` into two lines.crates/rustynes-mappers/src/m177_hengedianzi.rs#L5-L5: splittwo windows.//!into two lines.crates/rustynes-mappers/src/m179_hengedianzi.rs#L6-L6: splitordinary 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-L11crates/rustynes-mappers/src/m177_hengedianzi.rs#L5-L5crates/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 thetestsmodule; 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-L278crates/rustynes-mappers/src/m177_hengedianzi.rs#L190-L191crates/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 againstcount * PRG_BANK_32K(32768 whencount == 1), not the actual image length. For a 16 KiB PRG,addr = 0xC000yields(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_prgto require exactly 32 KiB for mapper 136):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."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()] }📝 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-L200crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53crates/rustynes-mappers/src/m240_cne_multicart.rs#L177-L183crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53crates/rustynes-mappers/src/m244_cne_decathlon.rs#L195-L207crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L42-L59crates/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-L5crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186crates/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 atMapperTier::BestEffort, buttier.rsplaces ids 246 and 250 inCURATED_IDSand its classifier arm returnsMapperTier::Curated(asserted byall_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::BestEffortto Curated (Tier-1).crates/rustynes-mappers/src/m250_nitra250.rs#L9-L16: change "best-effort (Tier-2)" /MapperTier::BestEffortto 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 - 1underflows on a single-bank PRG image.
new()accepts any non-zero multiple of 8 KiB, so an 8 KiB PRG giveslast == 0. The arm(false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1then computes0usize - 1, which panics in debug builds on a normal$C000read. Release builds are saved only by the later% countinread_prg. This contradicts the module's own "cannot afford a panic on a register access" invariant;m004_mmc3avoids it withsaturating_sub.As per coding guidelines: outside `#[cfg(test)]`, do not add code that can `panic!()` on data parsed from ROMs; return/compute safely instead.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, }📝 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 precedesNtdec63; also fix the "Mapper 76" (L246), "Mapper 225" (L411), "Mapper 95" (L727), "Mapper 137" (L901), "Bmc204/204" (L1123), and "Bmc11160/299" (L1327) blocks to matchNtdec174/mapper-40/NtdecAsder112/NtdecTc112(193)/NtdecN625092(221)/test-module respectively.crates/rustynes-mappers/src/sachen_8259.rs#L58-L74: "Mapper 40 — NTDEC 2722" precedesSachen8259M137; 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" precedesSachen133; also fix "Mapper 180" (L1273) beforeSachenTca01M143and "Mapper 177" (L1415) before the test module.crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedesWaixing242; also fix "Mapper 246" (L270) beforeWaixingFs304M162and "Mapper 244" (L647) beforeWaixing253.crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344: "TxcChip … Sachen 3011 / m136" block precedes the test module; there is noTxcChipin this file.crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780: "Sachen 8259 (A/B/C)" block precedes the test module; that family lives insachen_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-L74crates/rustynes-mappers/src/sachen_discrete.rs#L62-L74crates/rustynes-mappers/src/waixing.rs#L63-L81crates/rustynes-mappers/src/m513_sachen_9602.rs#L341-L344crates/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,400count includes dot 0 on each visible scanline, but the text labels it only as dots257..=340; it also omits the post-render line and therefore does not explain the stated27,902remainder. Use the exact breakdown: visible-line dot 0 plus dots257..=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
.rnsfiles 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
IndexErrorinstead 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/RustyNESand overwrite a predictable output name. A local attacker can pre-create that directory and symlinkan_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 ofdot2val.get(d, None)Replace
dot2val.get(d, None)withdot2val.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-L48scripts/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_abortexit or missingSTRESS2007=line reaches Line 8 and raises an opaqueIndexError; 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:
subprocesscall: check for execution of untrusted input(S603)
[warning] 7-7:
subprocess.runwithout explicitcheckargumentAdd explicit
check=False(PLW1510)
[warning] 8-8: Prefer
next(l for l in out.splitlines() if 'STRESS2007=' in l)over single element sliceReplace 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 concatenationReplace 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>raisesIndexErrorinstead of printing usage and exiting with a command-line error. Validatesys.argvbefore 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 withFileNotFoundErrorafter 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 ofrowsandpromoted_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
.nesmember 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.nesmembers 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.mdidentifies this fixture ascpu_timing_test6/cpu_timing_test.nes, so calling it “more comprehensive thancpu_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 remainingcrates/rustynes-mappers/src/sprintN.rsreference at Line 364, or mark it explicitly as historical.to-dos/ROADMAP.md#L334-L340: replacecrates/rustynes-mappers/src/mmc2_mmc4.rswithcrates/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, sobank_count - 2(slot 2,$C000) underflowsusizebefore the% bank_countruns — a debug panic on acpu_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 (... infds.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-L96crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70crates/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 - 2underflows for a single-bank (8 KiB) PRG.
newaccepts any non-zero multiple of 8 KiB, sobank_countcan be 1. A$C000-$FFFFread then hits slot 2 →1 - 2, which panics in debug builds and wraps in release. Siblingm076_namcot3446.rs(Line 138-140) already documents this exact single-8 KiB-bank case and guards it withsaturating_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.rsRepository: 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. Use1..=PPU_SNAPSHOT_VERSIONso 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.
|
@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. |
|
✅ Action performedFull review finished. |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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 winRemove duplicated mapper-lineage phrases.
Line 1464 repeats
NTDEC/TXC/BMCon the following line, and lines 1478-1479 repeatreusable-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 winStale doc comment: this "future refinement" is now implemented in the same file.
The
sample_lightdocstring still describes full per-dot temporal integration against the beam position as a "documented future refinement," butlight_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
⛔ Files ignored due to path filters (33)
Cargo.lockis excluded by!**/*.lock,!Cargo.lockcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snapis 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.gitignoreAGENTS.mdCHANGELOG.mdCargo.tomlREADME.mdcrates/rustynes-apu/src/snapshot.rscrates/rustynes-core/Cargo.tomlcrates/rustynes-core/benches/full_frame.rscrates/rustynes-core/benches/snapshot_restore.rscrates/rustynes-core/src/bus.rscrates/rustynes-core/src/input_device.rscrates/rustynes-core/src/nes.rscrates/rustynes-frontend/src/app.rscrates/rustynes-frontend/src/config.rscrates/rustynes-frontend/src/debugger/settings_panel.rscrates/rustynes-frontend/src/history_viewer.rscrates/rustynes-frontend/src/runahead.rscrates/rustynes-mappers/src/bmc_simple.rscrates/rustynes-mappers/src/fds.rscrates/rustynes-mappers/src/homebrew_boards.rscrates/rustynes-mappers/src/jaleco_discrete.rscrates/rustynes-mappers/src/kaiser.rscrates/rustynes-mappers/src/lib.rscrates/rustynes-mappers/src/m000_nrom.rscrates/rustynes-mappers/src/m001_mmc1.rscrates/rustynes-mappers/src/m002_uxrom.rscrates/rustynes-mappers/src/m003_cnrom.rscrates/rustynes-mappers/src/m004_mmc3.rscrates/rustynes-mappers/src/m005_mmc5.rscrates/rustynes-mappers/src/m007_axrom.rscrates/rustynes-mappers/src/m009_mmc2.rscrates/rustynes-mappers/src/m010_mmc4.rscrates/rustynes-mappers/src/m011_color_dreams.rscrates/rustynes-mappers/src/m013_cprom.rscrates/rustynes-mappers/src/m016_bandai_fcg.rscrates/rustynes-mappers/src/m018_jaleco_ss88006.rscrates/rustynes-mappers/src/m019_namco163.rscrates/rustynes-mappers/src/m021_vrc4.rscrates/rustynes-mappers/src/m022_vrc2.rscrates/rustynes-mappers/src/m024_vrc6.rscrates/rustynes-mappers/src/m032_irem_g101.rscrates/rustynes-mappers/src/m033_taito_tc0190.rscrates/rustynes-mappers/src/m034_bnrom_nina001.rscrates/rustynes-mappers/src/m035_jy_asic.rscrates/rustynes-mappers/src/m036_txc_policeman.rscrates/rustynes-mappers/src/m038_bitcorp38.rscrates/rustynes-mappers/src/m039_subor39.rscrates/rustynes-mappers/src/m041_caltron41.rscrates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rscrates/rustynes-mappers/src/m048_taito_tc0690.rscrates/rustynes-mappers/src/m050_fds_conv_smb2j.rscrates/rustynes-mappers/src/m064_rambo1.rscrates/rustynes-mappers/src/m065_irem_h3001.rscrates/rustynes-mappers/src/m066_gxrom.rscrates/rustynes-mappers/src/m067_sunsoft3.rscrates/rustynes-mappers/src/m068_sunsoft4.rscrates/rustynes-mappers/src/m069_sunsoft_fme7.rscrates/rustynes-mappers/src/m070_bandai74.rscrates/rustynes-mappers/src/m071_camerica_bf9093.rscrates/rustynes-mappers/src/m073_vrc3.rscrates/rustynes-mappers/src/m075_vrc1.rscrates/rustynes-mappers/src/m076_namcot3446.rscrates/rustynes-mappers/src/m077_irem_napoleon.rscrates/rustynes-mappers/src/m078_irem_jaleco78.rscrates/rustynes-mappers/src/m079_ave_nina03_06.rscrates/rustynes-mappers/src/m080_taito_x1_005.rscrates/rustynes-mappers/src/m082_taito_x1_017.rscrates/rustynes-mappers/src/m085_vrc7.rscrates/rustynes-mappers/src/m087_jaleco87.rscrates/rustynes-mappers/src/m088_namco118.rscrates/rustynes-mappers/src/m089_sunsoft2.rscrates/rustynes-mappers/src/m093_sunsoft3r.rscrates/rustynes-mappers/src/m094_un1rom.rscrates/rustynes-mappers/src/m095_namcot3425.rscrates/rustynes-mappers/src/m096_bandai96.rscrates/rustynes-mappers/src/m097_irem_tam_s1.rscrates/rustynes-mappers/src/m099_vs_system.rscrates/rustynes-mappers/src/m107_magic_dragon107.rscrates/rustynes-mappers/src/m113_ave_nina006.rscrates/rustynes-mappers/src/m118_txsrom.rscrates/rustynes-mappers/src/m119_tqrom.rscrates/rustynes-mappers/src/m132_txc_22211.rscrates/rustynes-mappers/src/m136_sachen_3011.rscrates/rustynes-mappers/src/m151_konami_vs.rscrates/rustynes-mappers/src/m152_bandai152.rscrates/rustynes-mappers/src/m156_daou156.rscrates/rustynes-mappers/src/m176_bmc_fk23c.rscrates/rustynes-mappers/src/m177_hengedianzi.rscrates/rustynes-mappers/src/m179_hengedianzi.rscrates/rustynes-mappers/src/m180_nichibutsu180.rscrates/rustynes-mappers/src/m184_sunsoft1.rscrates/rustynes-mappers/src/m185_cnrom185.rscrates/rustynes-mappers/src/m210_namco175.rscrates/rustynes-mappers/src/m232_camerica_bf9096.rscrates/rustynes-mappers/src/m240_cne_multicart.rscrates/rustynes-mappers/src/m241_bxrom241.rscrates/rustynes-mappers/src/m244_cne_decathlon.rscrates/rustynes-mappers/src/m246_fong_shen_bang246.rscrates/rustynes-mappers/src/m250_nitra250.rscrates/rustynes-mappers/src/m268_bmc_coolboy.rscrates/rustynes-mappers/src/m513_sachen_9602.rscrates/rustynes-mappers/src/mapper.rscrates/rustynes-mappers/src/mmc3_clones.rscrates/rustynes-mappers/src/multicart_discrete.rscrates/rustynes-mappers/src/nsf.rscrates/rustynes-mappers/src/nsf_expansion.rscrates/rustynes-mappers/src/ntdec.rscrates/rustynes-mappers/src/sachen_8259.rscrates/rustynes-mappers/src/sachen_discrete.rscrates/rustynes-mappers/src/sprint10.rscrates/rustynes-mappers/src/sprint11.rscrates/rustynes-mappers/src/sprint12.rscrates/rustynes-mappers/src/sprint13.rscrates/rustynes-mappers/src/sprint2.rscrates/rustynes-mappers/src/sprint3.rscrates/rustynes-mappers/src/sprint5.rscrates/rustynes-mappers/src/sprint6.rscrates/rustynes-mappers/src/sprint7.rscrates/rustynes-mappers/src/sprint8.rscrates/rustynes-mappers/src/sprint9.rscrates/rustynes-mappers/src/tier.rscrates/rustynes-mappers/src/unif.rscrates/rustynes-mappers/src/waixing.rscrates/rustynes-mappers/tests/corpus.rscrates/rustynes-netplay/tests/determinism.rscrates/rustynes-netplay/tests/udp_loopback.rscrates/rustynes-ppu/Cargo.tomlcrates/rustynes-ppu/src/ppu.rscrates/rustynes-ppu/src/snapshot.rscrates/rustynes-test-harness/Cargo.tomlcrates/rustynes-test-harness/src/bin/pgo_trainer.rscrates/rustynes-test-harness/tests/accuracycoin_runahead.rscrates/rustynes-test-harness/tests/audio_expansion.rscrates/rustynes-test-harness/tests/cpu_reset.rscrates/rustynes-test-harness/tests/fast_dotloop_diff.rscrates/rustynes-test-harness/tests/holy_mapperel.rscrates/rustynes-test-harness/tests/mmc3_clone_a12.rscrates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rscrates/rustynes-test-harness/tests/movie.rscrates/rustynes-test-harness/tests/ppu_sprites.rscrates/rustynes-test-harness/tests/snapshot_schema_audit.rscrates/rustynes-test-harness/tests/visual_regression.rsdocs/STATUS.mddocs/accuracy-ledger.mddocs/adr/0034-ppu-snapshot-v8-sprite-eval-state.mddocs/apu-2a03.mddocs/compatibility.mddocs/expansion-audio.mddocs/frontend.mddocs/mappers.mddocs/performance.mddocs/ppu-2c02.mddocs/testing-strategy.mdscripts/agy-review.shscripts/bench_relative_check.shscripts/diag/README.mdscripts/diag/dma_loop_span_analyzer.pyscripts/diag/dma_sweep_analyzer.pyscripts/diag/dump_trace_csv.pyscripts/diag/locate_first_trace_divergence.pyscripts/diag/ppu2002_diff_traces.pyscripts/diag/ppu2002_isolated_exact_timed_reads.pyscripts/diag/ppu2002_prerender_vbl_reads.pyscripts/diag/ppu2002_read_context_window.pyscripts/diag/ppu2002_read_value_histogram.pyscripts/diag/ppu2004_diff_vs_answerkey.pyscripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.pyscripts/diag/ppu2004_mesen_vs_rustynes_scanline128.pyscripts/diag/ppu2007_fetch_bus_dump.pyscripts/diag/ppu2007_key_alignment_shift_scan.pyscripts/diag/ppu2007_offset_greedy_search.pyscripts/diag/ppu2007_pattern_value_sets.pyscripts/diag/ppu2007_phase_offset_ranking.pyscripts/diag/ppu2007_stress_per_index_evaluator.pyscripts/mapper-promotion/README.mdscripts/mapper-promotion/batch2.pyscripts/mapper-promotion/convert_gg.pyscripts/mapper-promotion/enumerate_staged.pyscripts/mapper-promotion/gen_promotion.pyscripts/mapper-promotion/mapper_scan.pyscripts/mapper-promotion/promo_tests.rsscripts/mapper-promotion/promo_tests_2.rsscripts/mapper-promotion/scan.pyscripts/perf/perf_capture.shscripts/pr-review/README.mdscripts/pr-review/list_all_threads.pyscripts/pr-review/list_unresolved_threads.pyscripts/release-automation/split_commit.shtests/roms/LICENSES.mdtests/roms/README.mdtests/roms/assorted/README.mdtests/roms/assorted/apu_01_len_ctr.nestests/roms/assorted/apu_02_len_table.nestests/roms/assorted/branch_timing_1_basics.nestests/roms/assorted/branch_timing_2_backward.nestests/roms/assorted/branch_timing_3_forward.nestests/roms/assorted/cpu_reset_ram_after_reset.nestests/roms/assorted/cpu_reset_registers.nestests/roms/assorted/cpu_timing_test.nestests/roms/assorted/flowing_palette.nestests/roms/assorted/full_palette.nestests/roms/assorted/nestest.nestests/roms/assorted/oam_read.nestests/roms/assorted/oam_stress.nestests/roms/mmc5/README.mdto-dos/DEFERRED-AND-CARRYOVER-FEATURES.mdto-dos/ROADMAP.md
There was a problem hiding this comment.
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 becausebool::default()isfalse; 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.mdRepository: 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)) PYRepository: doublegate/RustyNES
Length of output: 947
Move
-D warningsafter--in bothcargo clippycommands..github/release-notes/v2.2.3.md#L133-L134andCHANGELOG.md#L289-L291should usecargo clippy … -- -D warningsso 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.appwins overXcode_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.mdRepository: 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.mdRepository: doublegate/RustyNES
Length of output: 6334
Use one test total across both release docs.
CHANGELOG.mdsays 2219 passed / 0 failed, while.github/release-notes/v2.2.3.mdsays 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)computesy - 1..=y + 1andx - 1..=x + 1onusizewith no lower-bound guard; calling it withx == 0ory == 0would panic on overflow in debug builds. Not triggered by any current call site ((100,120),(50,60)), but worth a defensivesaturating_subfor 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 bothxandyoff-screen. Every tested scanline (0, 120, 239) is< y (250), solight_at_scanlineshort-circuits on thescanline < ybeam-gate before ever reachingaperture_is_bright'sax >= W || ay >= Hcheck. The test therefore proves the beam-gate works, not that off-screen aiming is rejected by the temporal path's aperture test — unlikezapper_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-L203crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43crates/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 inm113_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-L59crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L53-L59crates/rustynes-mappers/src/m095_namcot3425.rs#L53-L71crates/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 onmod 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-L223crates/rustynes-mappers/src/m079_ave_nina03_06.rs#L226-L227crates/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 computeL<<0 | H<<1— confirmed by the module's ownbit_swapped_chr_selecttest (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:
- 1: https://github.com/mamedev/mame/blob/453b8e7d/src/devices/bus/nes/sunsoft.cpp
- 2: https://www.nesdev.org/wiki/Bus_conflict
- 3: https://mamedev.emulab.it/etabeta/2009/09/30/nessundorma/
🏁 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.rsRepository: doublegate/RustyNES
Length of output: 18608
Mask Sunsoft-3R writes for bus conflicts.
This register is markedBUS 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 abovemod tests. The mapper-132 block inm097_irem_tam_s1.rsis 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 bym132_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-L65crates/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/srcRepository: doublegate/RustyNES
Length of output: 37096
Use
saturating_sub(1)for the$C000-$DFFFwindow.
With exactly one 8 KiB PRG bank,last == 0, solast - 1panics in debug builds beforeread_prgcan 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.rsand mapper 86'sJaleco86lives injaleco_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 thetestsmodule.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" precedesCamerica232(232); remove it (and the "Mapper 240" block at L191-200).crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53: "Mapper 38" precedesCne240(240); remove it (and the "Mapper 241" block at L177-183).crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41: "Mapper 38" precedesBxrom241(241); remove it.crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59: "Mapper 28 — Action 53" precedesFongShenBang246(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" precedesMulticart61; 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" precedesNtdec63; 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" precedesSachenTca01M143(143); realign.crates/rustynes-mappers/src/waixing.rs#L63-L81: "Mapper 28 — Action 53" precedescheck_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-L53crates/rustynes-mappers/src/m241_bxrom241.rs#L35-L41crates/rustynes-mappers/src/m246_fong_shen_bang246.rs#L41-L59crates/rustynes-mappers/src/mmc3_clones.rs#L767-L780crates/rustynes-mappers/src/multicart_discrete.rs#L295-L301crates/rustynes-mappers/src/ntdec.rs#L60-L78crates/rustynes-mappers/src/sachen_8259.rs#L58-L74crates/rustynes-mappers/src/sachen_discrete.rs#L1273-L1282crates/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
# Errorsdoc states CHR-ROM is rejected, butnew()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 rendersusing.//!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 … Seetier.rs(MapperTier::BestEffort)"), buttier.rs::mapper_tierclassifies most (in several files, all) of the contained ids asCurated(Tier-1), and theCURATED_IDS/BEST_EFFORT_IDStests lock that in. Qualify the header (e.g. "verified with best-effort methodology; tier pertier.rsvaries by id") and drop the categoricalMapperTier::BestEffortcross-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-L16crates/rustynes-mappers/src/mmc3_clones.rs#L17-L25crates/rustynes-mappers/src/ntdec.rs#L12-L20crates/rustynes-mappers/src/sachen_8259.rs#L11-L19crates/rustynes-mappers/src/sachen_discrete.rs#L15-L23crates/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.rsRepository: doublegate/RustyNES
Length of output: 1741
Torture-ROM overstates
$2006/$2007idle-line coverage
crates/rustynes-test-harness/tests/fast_dotloop_diff.rs:246-316—$2006and$2007only armcopy_v_delay/ppudata_sm_countdownon render scanlines, so this vblank loop never actually drives those counters on idle-line dots. Only$2001reliably exercisesmask_write_delayhere. 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=0000labels 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
i32mixer contract.The trait was widened to
i32, but both pages retaini16contract text;docs/expansion-audio.mdalso 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 earlierMapper::mix_audio(&mut self) -> i16signature toi32.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.rsandcne240.rsdo not match this PR’s board-named modules: usem079_ave_nina03_06.rsandm240_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 -clands in process argv.
AUTHORIZATION: bearer ${GH_TOKEN}is embedded directly in thegit -ccommand-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 reasonactions/checkoutavoids 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
IndexErrorrather 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/**/*.pyprioritizes “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
0xE0masks; 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_abortoutput explicitly.
scan_dma_abortmay emitSTRESS2007=(unset), and command failures or a missing line currently becomeIndexError/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:
subprocesscall: check for execution of untrusted input(S603)
[warning] 7-7:
subprocess.runwithout explicitcheckargumentAdd explicit
check=False(PLW1510)
[warning] 8-8: Prefer
next(l for l in out.splitlines() if 'STRESS2007=' in l)over single element sliceReplace 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)returnsNone, so:02XraisesTypeErrorand 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.pydoes not exist, while invoking this script without a dump path crashes atsys.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 toroms[0].scripts/mapper-promotion/gen_promotion.py#L59-L60: derive Curated and remaining BestEffort IDs from successfully validated blocks, not the staticPROMOTElist.scripts/mapper-promotion/enumerate_staged.py#L60-L65: retain the mismatch report, but exclude the fallback candidate fromrowsand 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-L60scripts/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.rsand 30 inscripts/mapper-promotion/promo_tests_2.rs), not 86. They also include non-commercial inputs, such asMagic Floor ... (PD)inscripts/mapper-promotion/promo_tests.rsLine 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, andc['author']['login']fails when the reviewer account has been deleted or is unavailable. Use the defensive pattern already present inlist_all_threads.pyand accessdatabaseId/bodywith 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
reviewThreadsbefore claiming the audit covers every thread.
reviewThreads(first:100)only supplies the first page, while the table below promises thatlist_all_threads.pyprints 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' cratesRepository: 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/mmc5Repository: doublegate/RustyNES
Length of output: 722
Fix the broken MMC5 path
crates/nes-test-harness/tests/m005_mmc5.rsdoes 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.rsfrom the parenthetical leaves the same entry’s “168 families on main” claim andcrates/rustynes-mappers/src/sprintN.rspath stale. The current release documents 172 mapper IDs andmNNN_<board>.rsmodules; 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.rsandm010_mmc4.rs, but this reference still namescrates/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_lightis mis-copied fromset_expansion_device.Lines 1631-1637 describe attaching/replacing a per-port overlay device and carry a
# Panicsclause ("Panics ifportis not in0..=1").set_zapper_temporal_light(&mut self, on: bool)takes noportand 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
dipcan overflow the M286 shift on restore.
self.dipis only ever set from a save-state (load_stateL486 assignsdata[c+3]unmasked). With a corrupted/foreign blob,self.dip + 4overflows theu8add (debug panic) fordip >= 252, and1u16 << (dip+4)overflows the shift (debug panic; release silently masks to a wrong enable mask) fordip >= 12. This is reachable from any M286$A000write 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
u8add 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(), orpanic!()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, andMMC5_MIX_BIASare alli16, so(pulse_mix + pcm_mix) - MMC5_MIX_BIASis fully evaluated ini16and only widened afterward. Today the peak (24580pre-bias) fits, but thei32return then buys no headroom — a later bump toMMC5_PULSE_SCALE/MMC5_PCM_SCALEwould 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/srcRepository: 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.rsRepository: 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.rsRepository: doublegate/RustyNES
Length of output: 3601
Serialize
prg_ramin these mapper snapshots. The core round-trips mapper state only throughMapper::save_state/load_state, so these boards lose WRAM on restore. Appendprg_ramto the payload and restore it here, matchingm005_mmc5.rsandm018_jaleco_ss88006.rs.
crates/rustynes-mappers/src/m010_mmc4.rs#L242-L286crates/rustynes-mappers/src/m021_vrc4.rs#L401-L458crates/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-L458crates/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.rsRepository: 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.rsRepository: 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.rsRepository: 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 reachesbank_count - 2and panics in debug builds. Usesaturating_sub(2)/saturating_sub(1)or reject< 2PRG banks innew.
crates/rustynes-mappers/src/m033_taito_tc0190.rs#L106-L117crates/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
Bnromvariantchr_is_ramis true andppu_writemutatesself.chr, but neithersave_statenorload_statetouchesself.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) appendchrwhenchr_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-L12crates/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 aboveTxc36(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 toBitcorp38; remove.crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96: "Mmc3Clone — reusable MMC3-style core" block precedesMapper42, 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 inm050_fds_conv_smb2j.rs; remove.crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70: "Mmc3Clone" block precedesMapper50(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-L196crates/rustynes-mappers/src/m038_bitcorp38.rs#L161-L167crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L80-L96crates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rs#L300-L310crates/rustynes-mappers/src/m050_fds_conv_smb2j.rs#L54-L70crates/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-L70crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451crates/rustynes-mappers/src/m176_bmc_fk23c.rs#L541-L549crates/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.rsRepository: doublegate/RustyNES
Length of output: 1245
PRG reads need to wrap by the actual PRG length
check_prgonly requires an 8 KiB multiple, but this read wraps againstcount * 32 KiB. For 8/16/24 KiB PRG images, that divisor is larger thanprg_rom.len(), soself.prg_rom[...]can index past the slice and panic. Wrap byself.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 theMapper 40 — NTDEC 2722block (and theMapper 162 — Waixing FS304block at L229-244); the file implements only mapper 156.crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60: delete theMapper 31block (and theMapper 58block at L183-193); the file implements only mapper 179.crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58: delete theMapper 147 — Sachen 3018block (and theMapper 185 — CNROMblock at L215-229; mapper 185 lives inm185_cnrom185.rs).crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62: delete theMapper 147block (and theMapper 200block 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-L60crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58crates/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 renderswrite.//!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.rsRepository: 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" doneRepository: 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 toMirroring’s declaration order, whileload_statealready decodes through a fixed0..5mapping. Use explicitmirroring_to_byte/byte_to_mirroringhelpers 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 - 1underflows for a single-8 KiB-bank PRG.
last = (len / 8 KiB).max(1) - 1, so an 8 KiB PRG giveslast == 0, and the arm(false, 0xC000..=0xDFFF) | (true, 0x8000..=0x9FFF) => last - 1evaluates0usize - 1— a panic under overflow checks (the% countinread_prgonly sanitizes the result after the subtraction).check_prgpermits 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.
Adjudication of the CodeRabbit pass — all 44 findingsThe inline comments never posted ("Inline review comments failed to post ... GitHub's Every finding was checked against the tree rather than accepted on its face. That mattered: Fixed — correctnessFour of these are pre-existing (present at the merge base under the old
Worth flagging on m093: the fix broke two unit tests, because Fixed — split damage, and there was more of it than the review foundCodeRabbit listed 17 orphaned "Mapper NN" dividers. Rather than trust or dismiss that, I ran Fixed — documentation that contradicted the code or itself
Fixed — tooling
Rejected — one false positive
Deferred, with a documented ledger entry — VRC7 OPLL snapshot stateGenuine: Deliberately not patched in this cut. Verification on this treeA note for the next run: the four custom pre-merge checks were inconclusive on a repository |
|
I’ll avoid duplicating CI-covered formatting and lint feedback unless it indicates a correctness issue. ✅ Action performedFull 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. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (33)
Cargo.lockis excluded by!**/*.lock,!Cargo.lockcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_clip_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_dac_square.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_apu.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_mmc5.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_n163.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6a.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc6b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_db_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_envelope_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_noise_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_patch_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_phase_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_sweep_5b.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_n163_longwave.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_test_vrc7.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/audio_expansion__audio_expansion_tri_silence.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_fme7_mr_gimmick.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_bandit_kings_of_ancient_china.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_castlevania_3.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc5_uchuu_keibitai_sdf.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6a_castlevania_3_retranslation.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_esper_dream_2.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/external_real_games__external_vrc6b_madara.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/holy_mapperel__holy_mapperel_bank_reachability.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snapis excluded by!**/*.snapcrates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snapis 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.gitignoreAGENTS.mdCHANGELOG.mdCargo.tomlREADME.mdcrates/rustynes-apu/src/snapshot.rscrates/rustynes-core/Cargo.tomlcrates/rustynes-core/benches/full_frame.rscrates/rustynes-core/benches/snapshot_restore.rscrates/rustynes-core/src/bus.rscrates/rustynes-core/src/input_device.rscrates/rustynes-core/src/nes.rscrates/rustynes-frontend/src/app.rscrates/rustynes-frontend/src/config.rscrates/rustynes-frontend/src/debugger/settings_panel.rscrates/rustynes-frontend/src/history_viewer.rscrates/rustynes-frontend/src/runahead.rscrates/rustynes-mappers/src/bmc_simple.rscrates/rustynes-mappers/src/fds.rscrates/rustynes-mappers/src/homebrew_boards.rscrates/rustynes-mappers/src/jaleco_discrete.rscrates/rustynes-mappers/src/kaiser.rscrates/rustynes-mappers/src/lib.rscrates/rustynes-mappers/src/m000_nrom.rscrates/rustynes-mappers/src/m001_mmc1.rscrates/rustynes-mappers/src/m002_uxrom.rscrates/rustynes-mappers/src/m003_cnrom.rscrates/rustynes-mappers/src/m004_mmc3.rscrates/rustynes-mappers/src/m005_mmc5.rscrates/rustynes-mappers/src/m007_axrom.rscrates/rustynes-mappers/src/m009_mmc2.rscrates/rustynes-mappers/src/m010_mmc4.rscrates/rustynes-mappers/src/m011_color_dreams.rscrates/rustynes-mappers/src/m013_cprom.rscrates/rustynes-mappers/src/m016_bandai_fcg.rscrates/rustynes-mappers/src/m018_jaleco_ss88006.rscrates/rustynes-mappers/src/m019_namco163.rscrates/rustynes-mappers/src/m021_vrc4.rscrates/rustynes-mappers/src/m022_vrc2.rscrates/rustynes-mappers/src/m024_vrc6.rscrates/rustynes-mappers/src/m032_irem_g101.rscrates/rustynes-mappers/src/m033_taito_tc0190.rscrates/rustynes-mappers/src/m034_bnrom_nina001.rscrates/rustynes-mappers/src/m035_jy_asic.rscrates/rustynes-mappers/src/m036_txc_policeman.rscrates/rustynes-mappers/src/m038_bitcorp38.rscrates/rustynes-mappers/src/m039_subor39.rscrates/rustynes-mappers/src/m041_caltron41.rscrates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rscrates/rustynes-mappers/src/m048_taito_tc0690.rscrates/rustynes-mappers/src/m050_fds_conv_smb2j.rscrates/rustynes-mappers/src/m064_rambo1.rscrates/rustynes-mappers/src/m065_irem_h3001.rscrates/rustynes-mappers/src/m066_gxrom.rscrates/rustynes-mappers/src/m067_sunsoft3.rscrates/rustynes-mappers/src/m068_sunsoft4.rscrates/rustynes-mappers/src/m069_sunsoft_fme7.rscrates/rustynes-mappers/src/m070_bandai74.rscrates/rustynes-mappers/src/m071_camerica_bf9093.rscrates/rustynes-mappers/src/m073_vrc3.rscrates/rustynes-mappers/src/m075_vrc1.rscrates/rustynes-mappers/src/m076_namcot3446.rscrates/rustynes-mappers/src/m077_irem_napoleon.rscrates/rustynes-mappers/src/m078_irem_jaleco78.rscrates/rustynes-mappers/src/m079_ave_nina03_06.rscrates/rustynes-mappers/src/m080_taito_x1_005.rscrates/rustynes-mappers/src/m082_taito_x1_017.rscrates/rustynes-mappers/src/m085_vrc7.rscrates/rustynes-mappers/src/m087_jaleco87.rscrates/rustynes-mappers/src/m088_namco118.rscrates/rustynes-mappers/src/m089_sunsoft2.rscrates/rustynes-mappers/src/m093_sunsoft3r.rscrates/rustynes-mappers/src/m094_un1rom.rscrates/rustynes-mappers/src/m095_namcot3425.rscrates/rustynes-mappers/src/m096_bandai96.rscrates/rustynes-mappers/src/m097_irem_tam_s1.rscrates/rustynes-mappers/src/m099_vs_system.rscrates/rustynes-mappers/src/m107_magic_dragon107.rscrates/rustynes-mappers/src/m113_ave_nina006.rscrates/rustynes-mappers/src/m118_txsrom.rscrates/rustynes-mappers/src/m119_tqrom.rscrates/rustynes-mappers/src/m132_txc_22211.rscrates/rustynes-mappers/src/m136_sachen_3011.rscrates/rustynes-mappers/src/m151_konami_vs.rscrates/rustynes-mappers/src/m152_bandai152.rscrates/rustynes-mappers/src/m156_daou156.rscrates/rustynes-mappers/src/m176_bmc_fk23c.rscrates/rustynes-mappers/src/m177_hengedianzi.rscrates/rustynes-mappers/src/m179_hengedianzi.rscrates/rustynes-mappers/src/m180_nichibutsu180.rscrates/rustynes-mappers/src/m184_sunsoft1.rscrates/rustynes-mappers/src/m185_cnrom185.rscrates/rustynes-mappers/src/m210_namco175.rscrates/rustynes-mappers/src/m232_camerica_bf9096.rscrates/rustynes-mappers/src/m240_cne_multicart.rscrates/rustynes-mappers/src/m241_bxrom241.rscrates/rustynes-mappers/src/m244_cne_decathlon.rscrates/rustynes-mappers/src/m246_fong_shen_bang246.rscrates/rustynes-mappers/src/m250_nitra250.rscrates/rustynes-mappers/src/m268_bmc_coolboy.rscrates/rustynes-mappers/src/m513_sachen_9602.rscrates/rustynes-mappers/src/mapper.rscrates/rustynes-mappers/src/mmc3_clones.rscrates/rustynes-mappers/src/multicart_discrete.rscrates/rustynes-mappers/src/nsf.rscrates/rustynes-mappers/src/nsf_expansion.rscrates/rustynes-mappers/src/ntdec.rscrates/rustynes-mappers/src/sachen_8259.rscrates/rustynes-mappers/src/sachen_discrete.rscrates/rustynes-mappers/src/sprint10.rscrates/rustynes-mappers/src/sprint11.rscrates/rustynes-mappers/src/sprint12.rscrates/rustynes-mappers/src/sprint13.rscrates/rustynes-mappers/src/sprint2.rscrates/rustynes-mappers/src/sprint3.rscrates/rustynes-mappers/src/sprint5.rscrates/rustynes-mappers/src/sprint6.rscrates/rustynes-mappers/src/sprint7.rscrates/rustynes-mappers/src/sprint8.rscrates/rustynes-mappers/src/sprint9.rscrates/rustynes-mappers/src/tier.rscrates/rustynes-mappers/src/unif.rscrates/rustynes-mappers/src/waixing.rscrates/rustynes-mappers/tests/corpus.rscrates/rustynes-netplay/tests/determinism.rscrates/rustynes-netplay/tests/udp_loopback.rscrates/rustynes-ppu/Cargo.tomlcrates/rustynes-ppu/src/ppu.rscrates/rustynes-ppu/src/snapshot.rscrates/rustynes-test-harness/Cargo.tomlcrates/rustynes-test-harness/src/bin/pgo_trainer.rscrates/rustynes-test-harness/tests/accuracycoin_runahead.rscrates/rustynes-test-harness/tests/audio_expansion.rscrates/rustynes-test-harness/tests/cpu_reset.rscrates/rustynes-test-harness/tests/fast_dotloop_diff.rscrates/rustynes-test-harness/tests/holy_mapperel.rscrates/rustynes-test-harness/tests/mmc3_clone_a12.rscrates/rustynes-test-harness/tests/mmc3_r1r2_phase_probe.rscrates/rustynes-test-harness/tests/movie.rscrates/rustynes-test-harness/tests/ppu_sprites.rscrates/rustynes-test-harness/tests/snapshot_schema_audit.rscrates/rustynes-test-harness/tests/visual_regression.rsdocs/STATUS.mddocs/accuracy-ledger.mddocs/adr/0034-ppu-snapshot-v8-sprite-eval-state.mddocs/apu-2a03.mddocs/compatibility.mddocs/expansion-audio.mddocs/frontend.mddocs/mappers.mddocs/performance.mddocs/ppu-2c02.mddocs/testing-strategy.mdscripts/agy-review.shscripts/bench_relative_check.shscripts/diag/README.mdscripts/diag/dma_loop_span_analyzer.pyscripts/diag/dma_sweep_analyzer.pyscripts/diag/dump_trace_csv.pyscripts/diag/locate_first_trace_divergence.pyscripts/diag/ppu2002_diff_traces.pyscripts/diag/ppu2002_isolated_exact_timed_reads.pyscripts/diag/ppu2002_prerender_vbl_reads.pyscripts/diag/ppu2002_read_context_window.pyscripts/diag/ppu2002_read_value_histogram.pyscripts/diag/ppu2004_diff_vs_answerkey.pyscripts/diag/ppu2004_diff_vs_answerkey_rerun_capture.pyscripts/diag/ppu2004_mesen_vs_rustynes_scanline128.pyscripts/diag/ppu2007_fetch_bus_dump.pyscripts/diag/ppu2007_key_alignment_shift_scan.pyscripts/diag/ppu2007_offset_greedy_search.pyscripts/diag/ppu2007_pattern_value_sets.pyscripts/diag/ppu2007_phase_offset_ranking.pyscripts/diag/ppu2007_stress_per_index_evaluator.pyscripts/mapper-promotion/README.mdscripts/mapper-promotion/batch2.pyscripts/mapper-promotion/convert_gg.pyscripts/mapper-promotion/enumerate_staged.pyscripts/mapper-promotion/gen_promotion.pyscripts/mapper-promotion/mapper_scan.pyscripts/mapper-promotion/promo_tests.rsscripts/mapper-promotion/promo_tests_2.rsscripts/mapper-promotion/scan.pyscripts/perf/perf_capture.shscripts/pr-review/README.mdscripts/pr-review/list_all_threads.pyscripts/pr-review/list_unresolved_threads.pyscripts/release-automation/split_commit.shtests/roms/LICENSES.mdtests/roms/README.mdtests/roms/assorted/README.mdtests/roms/assorted/apu_01_len_ctr.nestests/roms/assorted/apu_02_len_table.nestests/roms/assorted/branch_timing_1_basics.nestests/roms/assorted/branch_timing_2_backward.nestests/roms/assorted/branch_timing_3_forward.nestests/roms/assorted/cpu_reset_ram_after_reset.nestests/roms/assorted/cpu_reset_registers.nestests/roms/assorted/cpu_timing_test.nestests/roms/assorted/flowing_palette.nestests/roms/assorted/full_palette.nestests/roms/assorted/nestest.nestests/roms/assorted/oam_read.nestests/roms/assorted/oam_stress.nestests/roms/mmc5/README.mdto-dos/DEFERRED-AND-CARRYOVER-FEATURES.mdto-dos/ROADMAP.md
There was a problem hiding this comment.
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 headline2237value 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: update2219or 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-L123CHANGELOG.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 actualdefault_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.*.appinto the loop. The final[ -d "$d" ]returns1, causingxc=$(...)to fail before the warning/fallback at Line 82. Wrap the test inif [ -d "$d" ]; then ... fiso filtered-out candidates do not fail the workflow.Proposed fix
- [ -d "$d" ] && printf '%s\n' "$d" + if [ -d "$d" ]; then + printf '%s\n' "$d" + fiThe 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
dipfrom an untrusted save state can panic the shift incpu_write.
load_staterestoresself.dip = data[c + 3]without bounding it, and the M286 path evaluates1u16 << (self.dip + 4). A corrupt or foreign blob withdip >= 12makes the shift amount>= 16(panic in debug / masked-and-wrong in release), anddip >= 252overflows theself.dip + 4u8 add. This defeats the module's stated no-panic-on-register-access guarantee for the#![no_std]stack.dipis never written by a legitimate register, so masking it on restore to its valid width is sufficient.As per coding guidelines: "do not add `.unwrap()`/`.expect()`/`panic!()` ... applied to untrusted data parsed from ... save states."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;📝 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 isMagicFloor218(mapper 218); UN1ROM lives inm094_un1rom.rs.crates/rustynes-mappers/src/homebrew_boards.rs#L515-L519: banner "Mapper 107 — Magic Dragon" precedesGtrom111(mapper 111).crates/rustynes-mappers/src/homebrew_boards.rs#L671-L680: banner "Mapper 234 — Maxi 15" precedesAction53M28(mapper 28).crates/rustynes-mappers/src/homebrew_boards.rs#L1184-L1202: banner "Mapper 63 — NTDEC 0324" precedes thetestsmodule; no mapper 63 exists in this file.crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182: banner "Mapper 101 — Jaleco JF-10" precedes thetestsmodule.crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56: banner "Mapper 15 — K-1029" precedesBandai96(mapper 96).crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244: banner "Mapper 97 — Irem TAM-S1" precedes thetestsmodule.crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65: banner "Mapper 15 — K-1029" precedesIrem97(mapper 97).crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L221-L233: banner "Mapper 132 — TXC 22211" precedes thetestsmodule.📍 Affects 4 files
crates/rustynes-mappers/src/homebrew_boards.rs#L194-L195(this comment)crates/rustynes-mappers/src/homebrew_boards.rs#L515-L519crates/rustynes-mappers/src/homebrew_boards.rs#L671-L680crates/rustynes-mappers/src/homebrew_boards.rs#L1184-L1202crates/rustynes-mappers/src/m094_un1rom.rs#L177-L182crates/rustynes-mappers/src/m096_bandai96.rs#L44-L56crates/rustynes-mappers/src/m096_bandai96.rs#L237-L244crates/rustynes-mappers/src/m097_irem_tam_s1.rs#L53-L65crates/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" precedesJaleco86(mapper 86); remove or retitle for mapper 86.crates/rustynes-mappers/src/jaleco_discrete.rs#L308-L322: "Mapper 41 — Caltron 6-in-1" precedes theJalecoLatch/Jaleco72/Jaleco92family; retitle for mappers 72/92.crates/rustynes-mappers/src/jaleco_discrete.rs#L631-L641: "Mapper 77 — Irem (Napoleon Senki)" precedesJaleco101; 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" precedesNitra250(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 tom513_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-L322crates/rustynes-mappers/src/jaleco_discrete.rs#L631-L641crates/rustynes-mappers/src/jaleco_discrete.rs#L756-L766crates/rustynes-mappers/src/m250_nitra250.rs#L42-L58crates/rustynes-mappers/src/m268_bmc_coolboy.rs#L445-L451crates/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 thetestsmodule. 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-$7FFFWRAM (MMC4's own comment notes Fire Emblem Gaiden reads its save magic from it) but neithersave_state/load_stateserializesprg_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.rsandm019_namco163.rsalready include their WRAM in the snapshot; mirror that here (appendprg_ramto the serialized tail and extend theexpectedlength inload_state).
crates/rustynes-mappers/src/m010_mmc4.rs#L242-L255: appendself.prg_ramaftervraminsave_state, add+ self.prg_ram.len()toload_state'sexpected, and copy it back in.crates/rustynes-mappers/src/m021_vrc4.rs#L401-L418: appendself.prg_ramaftervraminsave_state, add+ self.prg_ram.len()to thescalar_len + vramexpected, 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 thetestsmodule (same artifact asm009_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.rsRepository: 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.rsRepository: 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-*/srcRepository: 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.rsRepository: 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_stateonly cover bank registers and nametable VRAM. They skipprg_ramand, whenchr_is_ramis set, thechr_rombuffer, 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
newenforces 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 to1 + 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_tailboth key offTAIL_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-L610crates/rustynes-mappers/src/m107_magic_dragon107.rs#L40-L47crates/rustynes-mappers/src/m240_cne_multicart.rs#L47-L53crates/rustynes-mappers/src/m244_cne_decathlon.rs#L47-L53crates/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)]abovemod tests.crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186: keep one#[cfg(test)]abovemod tests.crates/rustynes-mappers/src/m244_cne_decathlon.rs#L209-L210: keep one#[cfg(test)]abovemod tests.📍 Affects 3 files
crates/rustynes-mappers/src/m095_namcot3425.rs#L251-L252(this comment)crates/rustynes-mappers/src/m240_cne_multicart.rs#L185-L186crates/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 - 1underflows for a single-bank PRG, panicking on every read.lastis(len/PRG_BANK_8K).max(1) - 1, so a 1-bank (8 KiB) ROM — whichnewaccepts — giveslast == 0, andread_prg(last - 1, …)at line 154 computes0usize - 1. Since this runs unconditionally at the top ofcpu_readfor 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% countinread_prgkeeps 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) andchr_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 - 1can underflow, violating the module's stated no-panic invariant.
newaccepts any non-zero 8 KiB multiple, so a single-bank (8 KiB) image giveslast = 0;cpu_read($C000..)withprg_mode == falsethen evaluateslast - 1as0usize - 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". Usesaturating_sub, matchingCoolboy::prg_bank_mmc3inm268_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:countis in 16 KiB units andlo16is wrapped as a 16 KiB index (% count), but the fetch spans a full 32 KiB (offup to0x7FFF). For an odd number of 16 KiB banks (e.g. 48 KiB →count == 3,page2/3 →lo16 == count-1),lo16 * PRG_BANK_16K + offreacheslen + 0x3FFF— a direct out-of-bounds index that panics in debug and release. A 48 KiB PRG passesis_multiple_of(PRG_BANK_16K), so untrusted ROMs reach it, violating the module's% countno-OOB invariant. Fix by masking the final index againstprg_rom.len()(or compute in 32 KiB units asMulticart61/Multicart62do).
crates/rustynes-mappers/src/multicart_discrete.rs#L1011-L1015: inMulticart202::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 inMulticart212::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 - 1underflows for a single-bank (8 KiB) PRG image.When
(prg_rom.len() / PRG_BANK_8K).max(1) == 1,last == 0, solast - 1underflows and a$C000-$DFFFfetch panics under overflow-checked (debug/test) builds. An 8 KiB PRG passes the constructor'sis_multiple_of(PRG_BANK_8K)check, so this is reachable from an untrusted ROM. The siblingNtdecTc112already usessaturating_subfor 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_VERSIONinstead of hardcoding8.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 makerestorereject the newest valid version withUnsupportedVersioninstead 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 newdetail=0000table 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_fieldonly 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 inself(e.g.myself.oam_addr,itself.oam_addr) would satisfy the check for fieldoam_addreven though it isn'tself.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_audiocontract documentation update. These pages now describe the widenedi32behavior but retain an obsoletei16signature, anddocs/expansion-audio.mdstill 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: replaceave_nina.rsandcne240.rswithm079_ave_nina03_06.rsandm240_cne_multicart.rs.tests/roms/mmc5/README.md#L32-L32: changecrates/nes-test-harness/tests/m005_mmc5.rstocrates/rustynes-test-harness/tests/m005_mmc5.rs; also update the adjacentnes-mapperscrate 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 viagit -con the command line expose it in process listings on shared/self-hosted CI runners the same waydocker run --envdoes?💡 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:
- 1: https://www.sourcery.ai/vulnerabilities/container-secrets-environment-variables
- 2: https://www.reddit.com/r/bash/comments/1rttlf0/stop_passing_secrets_as_commandline_arguments/
- 3: https://unix.stackexchange.com/questions/264681/how-to-stop-a-user-from-seeing-command-line-arguments
- 4: https://superuser.com/questions/415360/how-do-i-find-out-command-line-arguments-of-a-running-program
- 5: https://securetoolsguide.com/secure-environment-variable-management-guide/
- 6: https://www.matthewswong.com/en/blog/docker-secrets-production/
- 7: https://docs.gitlab.com/17.9/ci/variables/
- 8: https://safeguard.sh/resources/blog/docker-secrets-management
- 9: https://docker.github.io/docker-agent/guides/secrets/
🏁 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.mdRepository: doublegate/RustyNES
Length of output: 6653
Avoid putting
GH_TOKENin thegitargv
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 viaGIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0, orgit 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.00and 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
trystatements from both CSV parsers.Each site starts another
trybefore closing the preceding one, leaving an unterminated outertryand making the script fail to parse.
scripts/diag/dma_loop_span_analyzer.py#L8-L9: retain onetryparsingcycandkind, with a targetedexcept (ValueError, IndexError).scripts/diag/dma_sweep_analyzer.py#L8-L9: retain onetryparsingcyc,kind, andheld, with a targetedexcept (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-continuedetected, 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 prevtreats a valid prior cycle0as absent, omitting its next interval from both outputs. Useprev 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] += 1As 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
hnot 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$2002value is shown without<<<. Includevalin the comparison; this otherwise hides the flag-timing divergence thatscripts/diag/ppu2002_diff_traces.pyreports.🧰 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 anIndexError; 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:
subprocesscall: check for execution of untrusted input(S603)
[warning] 7-7:
subprocess.runwithout explicitcheckargumentAdd explicit
check=False(PLW1510)
[warning] 8-8: Prefer
next(l for l in out.splitlines() if 'STRESS2007=' in l)over single element sliceReplace 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
IndexErrorinstead 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_nesreturnsNonewhen no.nesentry is found (or could raise on a corrupt archive, since it has no try/except unlikerom_bytesin the sibling scripts).ines_mapper(None)then doeslen(b)and raisesTypeError, aborting the run instead of recording an entry inerrorslike theMISSING SRC/HEADER mismatchcases 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.pyRepository: doublegate/RustyNES
Length of output: 3660
Validate the GraphQL response before indexing
reviewThreads.Both helpers assume
data.repository.pullRequest.reviewThreads.nodesexists; a GraphQLerrorspayload or partial response will currently throw a traceback instead of a clear failure.
scripts/pr-review/list_all_threads.py#L2-L3scripts/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 || trueRepository: 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 -SRepository: 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 '') PYRepository: 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.pyRepository: 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 -nRepository: 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.pyRepository: doublegate/RustyNES
Length of output: 3254
Paginate
reviewThreadsor state the 100-thread limit.
reviewThreads(first:100)truncates larger PRs, so the closeout list can miss unresolved threads past the first page. AddpageInfo/endCursorhandling, 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 remainingsprintN.rspointer at Line 364 with current board-named paths or remove it.to-dos/ROADMAP.md#L335-L338: replace the adjacentmmc2_mmc4.rspointer at Line 340 withm010_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, noti16. The whole expression((p1 + p2 + saw) - 30) * VRC6_MIX_SCALEevaluates ini16(all three operands andVRC6_MIX_SCALEarei16); only the already-narrow result is widened. It stays in range today (30349/-29370), but the trait was widened toi32precisely to provide headroom — adding a channel or bumpingVRC6_MIX_SCALEwould 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-L167crates/rustynes-mappers/src/m039_subor39.rs#L40-L52crates/rustynes-mappers/src/m041_caltron41.rs#L37-L43crates/rustynes-mappers/src/m180_nichibutsu180.rs#L42-L58crates/rustynes-mappers/src/m185_cnrom185.rs#L46-L62crates/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-L59crates/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/srcRepository: 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.rsRepository: 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.rsRepository: 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.rsRepository: 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.rsRepository: 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/srcRepository: doublegate/RustyNES
Length of output: 50375
Route Namco 118 four-screen nametables through the full 4 KiB offset.
PpuBusAdapterfalls back toMapper::nametable_addressfor$2000-$3EFF, andNamco118does not implementnametable_fetch/nametable_write, so the PPU never uses the mapper's 4 KiBvram. Withfixed_4screen,off & 0x07FFaliases 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-L217crates/rustynes-mappers/src/m132_txc_22211.rs#L58-L70crates/rustynes-mappers/src/m132_txc_22211.rs#L268-L275crates/rustynes-mappers/src/m156_daou156.rs#L42-L58crates/rustynes-mappers/src/m156_daou156.rs#L229-L244crates/rustynes-mappers/src/m177_hengedianzi.rs#L52-L59crates/rustynes-mappers/src/m179_hengedianzi.rs#L53-L60crates/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/testsRepository: 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.rsRepository: 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_}') PYRepository: 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-$FFFFreads
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 againstself.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_enabledis not serialized, so a snapshot taken before the first register write does not round-trip.
chr_enabledpowers ontruewhilechr_reg_raw == 0, butload_statereconstructs it viachr_enable_for(chr_reg_raw), andchr_enable_for(0)isfalseunder 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 explicitchr_enabled = truewas added to avoid. This is observable for snapshot/run-ahead taken before the first$8000write.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.
There was a problem hiding this comment.
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 winUpdate 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 winCorrect the provenance-section reference.
The sentence points all upstream paths to the “blargg's NES test ROMs” section, but
nestest.nesis sourced fromother/nestest.nesand attributed to kevtris intests/roms/LICENSES.md. Point readers to the relevant entries generally, or explicitly mention the separatenestestentry.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 winMake the verification commands match the reported results.
The block shows only
cargo test --workspace --features test-roms, but also reportsexternal_real_gamesandexternal_extended, which require--features commercial-romsand 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 winUpdate 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 winFix the benchmark summary mismatch — the prose says all four workloads had
p > 0.05, butnes_run_frame_nestest_fastis listed withp = 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 liftAvoid retaining the entire trace in memory.
rowsretains 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 winPath Traversal (CWE-59)
Make the output write symlink-safe. The script still writes to the fixed
/tmp/RustyNES/an_out.txtpath, with no private directory setup orO_NOFOLLOW/O_EXCLguard. 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
📒 Files selected for processing (59)
.github/release-notes/v2.2.3.mdAGENTS.mdCHANGELOG.mdcrates/rustynes-core/src/bus.rscrates/rustynes-core/src/input_device.rscrates/rustynes-mappers/src/homebrew_boards.rscrates/rustynes-mappers/src/jaleco_discrete.rscrates/rustynes-mappers/src/m002_uxrom.rscrates/rustynes-mappers/src/m009_mmc2.rscrates/rustynes-mappers/src/m021_vrc4.rscrates/rustynes-mappers/src/m022_vrc2.rscrates/rustynes-mappers/src/m032_irem_g101.rscrates/rustynes-mappers/src/m033_taito_tc0190.rscrates/rustynes-mappers/src/m036_txc_policeman.rscrates/rustynes-mappers/src/m038_bitcorp38.rscrates/rustynes-mappers/src/m039_subor39.rscrates/rustynes-mappers/src/m041_caltron41.rscrates/rustynes-mappers/src/m042_fds_conv_bio_miracle.rscrates/rustynes-mappers/src/m048_taito_tc0690.rscrates/rustynes-mappers/src/m050_fds_conv_smb2j.rscrates/rustynes-mappers/src/m076_namcot3446.rscrates/rustynes-mappers/src/m077_irem_napoleon.rscrates/rustynes-mappers/src/m079_ave_nina03_06.rscrates/rustynes-mappers/src/m093_sunsoft3r.rscrates/rustynes-mappers/src/m094_un1rom.rscrates/rustynes-mappers/src/m095_namcot3425.rscrates/rustynes-mappers/src/m096_bandai96.rscrates/rustynes-mappers/src/m097_irem_tam_s1.rscrates/rustynes-mappers/src/m107_magic_dragon107.rscrates/rustynes-mappers/src/m113_ave_nina006.rscrates/rustynes-mappers/src/m132_txc_22211.rscrates/rustynes-mappers/src/m136_sachen_3011.rscrates/rustynes-mappers/src/m156_daou156.rscrates/rustynes-mappers/src/m177_hengedianzi.rscrates/rustynes-mappers/src/m179_hengedianzi.rscrates/rustynes-mappers/src/m180_nichibutsu180.rscrates/rustynes-mappers/src/m185_cnrom185.rscrates/rustynes-mappers/src/m232_camerica_bf9096.rscrates/rustynes-mappers/src/m240_cne_multicart.rscrates/rustynes-mappers/src/m241_bxrom241.rscrates/rustynes-mappers/src/m244_cne_decathlon.rscrates/rustynes-mappers/src/m246_fong_shen_bang246.rscrates/rustynes-mappers/src/m250_nitra250.rscrates/rustynes-mappers/src/multicart_discrete.rscrates/rustynes-mappers/src/ntdec.rscrates/rustynes-mappers/src/sachen_8259.rscrates/rustynes-mappers/src/sachen_discrete.rscrates/rustynes-mappers/src/waixing.rscrates/rustynes-ppu/src/snapshot.rsdocs/accuracy-ledger.mddocs/performance.mddocs/ppu-2c02.mdscripts/diag/locate_first_trace_divergence.pyscripts/diag/ppu2002_read_value_histogram.pyscripts/diag/ppu2007_stress_per_index_evaluator.pyscripts/pr-review/list_unresolved_threads.pytests/roms/assorted/README.mdto-dos/DEFERRED-AND-CARRYOVER-FEATURES.mdto-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
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).
There was a problem hiding this comment.
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 winValidate the nested GraphQL payload before iterating in
scripts/pr-review/list_unresolved_threads.py.or []turns a missingreviewThreads.nodesinto a false zero-count result, and direct indexing intothread["isResolved"],thread["comments"]["nodes"], andc["databaseId"]will still raiseKeyError/TypeErroron 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
📒 Files selected for processing (4)
crates/rustynes-core/src/bus.rsdocs/accuracy-ledger.mdscripts/diag/locate_first_trace_divergence.pyscripts/pr-review/list_unresolved_threads.py
…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.
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
Nes::set_fast_dotloophad 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.ymlalready computed a promotion verdict;release.ymlnever consumed it, so a passing gate still shipped a plain build. Wired, scoped honestly to linux-x86_64 where the gate runs.docs/performance.mdper 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
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 existingcpu_read_unmappedcontract.Mapper::mix_audiotoi32— the correct full-scale tone does not fiti16.PPU_SNAPSHOT_VERSION8 + 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.Housekeeping
sprintN.rsmapper modules (27,631 lines, ~110 boards) renamed for the boards they emulate withmNNN_prefixes. Verified content-preserving mechanically: 930 top-level items byte-identical, 0 altered, and an identical 172-ID dispatch table.expansion_level_tripwire, since that suite needs gitignored ROMs and so never runs in CI.Breaking
.rnssave 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_audioreturnsi32(wasi16). Out-of-tree mappers need the signature updated; every in-tree board returns the values it always did.Verification
Release mechanics
On merge,
release-auto.ymltagsv2.2.3, publishes with.github/release-notes/v2.2.3.mdas the body, and invokesrelease.ymlto 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