chore(ci): Bump softprops/action-gh-release from 1 to 2 - #3
Conversation
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](softprops/action-gh-release@v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '2' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
There was a problem hiding this comment.
Pull request overview
This PR updates the softprops/action-gh-release GitHub Action from version 1 to version 2. The primary motivation is to migrate from Node 16 to Node 20, addressing deprecation warnings in GitHub Actions workflows.
- Updates the action version reference in the release workflow
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
doublegate
left a comment
There was a problem hiding this comment.
Reviewed / Approved - GitHub Version Bump(s) -- DG 12/19
…ream A) + C1-RES re-baseline doc (#79) v1.3.0 "Bedrock" beta.1, part 1 of Workstream A (toolchain modernization). Moves the entire workspace to the Rust 2024 edition and records the hard-tier accuracy re-baseline. Determinism-neutral and fully verified. Edition 2021 -> 2024 - [workspace.package] edition + the two explicit-edition crates (rustynes-cheevos, rustynes-script) bumped to "2024". - Mechanical migration via `cargo fix --edition` across the default, test-roms, and frontend scripting/hd-pack feature sets, then `cargo fmt --all` (2024 style): * `extern "C" { ... }` FFI declaration blocks -> `unsafe extern "C" { ... }` (rustynes-cheevos/src/ffi.rs) — 2024 requires the `unsafe` on extern blocks. * disasm opcode-table macro fragment specifiers pinned to `expr_2021` (rustynes-cpu/src/disasm.rs) — preserves exact 2021 macro-matching semantics. * one `gen` local -> raw identifier `r#gen` (rustynes-frontend/src/audio.rs); `gen` is a reserved keyword in 2024. * removed a redundant block-return brace flagged by `unused_braces` (rustynes-core/src/bus.rs::oam_dma_overlap_ready). * imports reformatted to the 2024 rustfmt style (uppercase-first grouping) across the workspace — the bulk of the diff; purely cosmetic. - No `tail_expr_drop_order` restructuring was required, so there is no drop-order / determinism surface. MSRV stays 1.86 (edition 2024 stabilized in 1.85); the egui 0.34 / wgpu 26 / rfd 0.17 dependency-tier bump that needs a newer MSRV is a separate follow-up step in this workstream. Determinism + quality gates (all green) - AccuracyCoin 100% (139/139), `visual_regression` golden framebuffers byte-identical, `nestest` 0-diff, `cpu_interrupts_v2` 5/5 strict — all unchanged. - cargo check --workspace (default + test-roms); clippy --workspace --all-targets -D warnings; clippy -p rustynes-frontend --features scripting,hd-pack -D warnings; cargo fmt --all --check; no_std cross-compile (rustynes-core, thumbv7em-none-eabihf); markdownlint (v0.39.0). C1-RES — hard-tier accuracy re-baseline doc (v1.3.0 beta.1, doc-only) - docs/STATUS.md: added a dated (2026-06-15) re-baseline note to the authoritative "Accuracy residuals" section. Confirms `cpu_interrupts_v2` is 5/5 strict (closed), and that exactly three residuals remain (`mmc3_test_2/4` #3 + two `apu_reset`), which two independent root-cause diagnoses found share one cause — the integer 3-PPU-dots-per-CPU-cycle scheduler cannot represent the M2 sub-cycle phase, and Nes::reset() is a function-call reset. Closing all three is a single v2.0-scale fractional-master-clock + cycle-accurate-reset refactor (HIGH risk, 15+ rollbacks, ADR-0002 stop condition); maintainer decision is to keep deferring. CHANGELOG.md: [Unreleased] gains a "Changed" entry for the edition migration. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit RustyNES against the Nesdev "Emulator tests" + "Tricky-to-emulate games" indices and the christopherpow/nes-test-roms aggregator for committable tests beyond the 139 AccuracyCoin battery. - Wire the older `mmc3_test` v1 suite (6 sub-ROMs, blargg/kevtris PD, distinct from the already-wired mmc3_test_2): 1/2/3 strict-PASS; 4/5/6 pinned as documented expected-fail probes. They converge on the SAME ADR-0002 fractional-master-clock scanline-IRQ-cadence residual as mmc3_test_2/4 #3 — no new bug (sub-scanline IRQ cadence, deferred to v2.0). Probes assert the failure shape so a future fix flips them. - Add dpcmletterbox (Damian Yerrick, royalty-free) as a deterministic framebuffer-hash visual smoke — a DMC-IRQ-as-scanline-timer raster split with no mapper IRQ, so a sensitive DMC-IRQ + sprite-0 + NMI/DMC phase sentinel. - LICENSES.md + docs/testing-strategy.md + CHANGELOG [Unreleased] updated. No core change; AccuracyCoin 100% (139/139) and determinism hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…waveform scope (#103) * test(c1): v1.5.0 Workstream C1 — TASVideos / extended emulator-test pass Audit RustyNES against the Nesdev "Emulator tests" + "Tricky-to-emulate games" indices and the christopherpow/nes-test-roms aggregator for committable tests beyond the 139 AccuracyCoin battery. - Wire the older `mmc3_test` v1 suite (6 sub-ROMs, blargg/kevtris PD, distinct from the already-wired mmc3_test_2): 1/2/3 strict-PASS; 4/5/6 pinned as documented expected-fail probes. They converge on the SAME ADR-0002 fractional-master-clock scanline-IRQ-cadence residual as mmc3_test_2/4 #3 — no new bug (sub-scanline IRQ cadence, deferred to v2.0). Probes assert the failure shape so a future fix flips them. - Add dpcmletterbox (Damian Yerrick, royalty-free) as a deterministic framebuffer-hash visual smoke — a DMC-IRQ-as-scanline-timer raster split with no mapper IRQ, so a sensitive DMC-IRQ + sprite-0 + NMI/DMC phase sentinel. - LICENSES.md + docs/testing-strategy.md + CHANGELOG [Unreleased] updated. No core change; AccuracyCoin 100% (139/139) and determinism hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): v1.5.0 Workstream C2/C3 — replay/TAS window + NSF scope C2 — Replay / TAS window (Tools -> Replay / TAS), modelled on GeraNES's ReplayWindowUI: - device-topology read-out (per-port controller/peripheral + Four Score), - timebase read-out (region + whole-Hz + elapsed/total wall-clock), - Record/Play/Branch/Stop controls (mirror F6/F7/F8) + deterministic seek-to-frame (slider + Start/-10/+1/+10). The app pushes a read-only ReplayInfo snapshot each frame and drains a ReplayRequest after the egui pass. Seek re-derives state via MovieUi::seek_playback (seek_to_start + replay recorded inputs), proven bit-identical to linear playback by a new round-trip test — no new determinism surface. C3 — NSF waveform visualizer: a per-channel oscilloscope (pulse1/2, triangle, noise, DMC) in the NSF Player window, fed from the read-only apu_snapshot() DAC levels; surfaces the expansion-audio chip name when present. Output-only eye-candy; no synthesis change. Frontend-only, additive/off-by-default. All clippy flavors (default + scripting,hd-pack + retroachievements + both wasm) + fmt + rustdoc + no_std clean; AccuracyCoin 100% (139/139) and determinism hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(debugger): adopt Gemini #103 review — seek-slider tracking + NSF no-alloc - replay_panel.rs: the seek slider now tracks the live playback cursor (it was static — `seek_target` only updated when it exceeded `last`, which never happens since the slider clamps to `0..=last`). Added a `seek_dragging` flag so live tracking pauses only while the user drags, then resumes after the seek. - nsf_panel.rs: `show_or_dash` is now a `fn` returning a borrowed `&str` (no per-frame `String` alloc for Title/Artist/Copyright). A `fn` (not a closure) so the return lifetime elides to the input — the closure form Gemini suggested fails borrowck (closures don't elide the return lifetime). Frontend-only, determinism-neutral. clippy (default + scripting,hd-pack + retroachievements + wasm) + fmt + AccuracyCoin 139/139 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Adjudicated (post-hoc closeout): Copilot's review is an overview of a routine Dependabot bump — |
fix(frontend): apply HD-pack tile substitution at render (align runtime key with real-Mesen format) (v1.7.1 #3)
Bounded-effort disposition for the MMC3 residuals, per the plan's maintainer-pre-authorized escape hatch: with the one-clock (A1) + every-cycle-bus-access (A2) + cycle-accurate-reset (A4) substrate complete, the free-flip check ran both residual families flag-on — - R1 (mmc3_test_2/4 + mmc3_test v1/4 sub-test #3, the 1-CPU-cycle "IRQ sooner when $2000=$08" bracket): unmoved. - R2 (mmc3_test v1/5 + v1/6 sub-test #2, reload-to-0 cadence + MMC6): unmoved. Neither flips on the substrate alone: closing them requires the dedicated M2-phase sample-point campaign (the irq_trace cross-diff axis behind 17 documented rollbacks). Per Risks #3 ("do not risk a 16th rollback of the sacred 100% for the 1-cycle bracket — this is the one place 'aggressive' yields to 'sacred'") and the maintainer's confirmed threshold ("if R1/R2 don't flip cleanly within the beta.3 window without regressing the gate, accept the by-design #[ignore] and ship"), the four pins stay #[ignore]'d by design with the disposition recorded in each reason string. Zero production-ROM impact. The rc.1 ADR-0002 update will mark the axis by-design-deferred (the plan's anticipated branch). The `_currently_fails` companions remain fail-loud: if a future substrate change flips either family, the pins demand the strict flip immediately. Verification: mmc3 + apu_reset suites green in both configs (the four escape-hatched pins ignored with the new reasons; everything else strict); clippy flag-on clean; CHANGELOG updated in the same change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fied; R1/R2 escape-hatched (#219) * feat(v2.0.0): beta.3 — A4 cycle-accurate reset closes R4; R3 reclassified Workstream A4 of the v2.0.0 "Timebase" plan (ADR 0002): the warm reset becomes a cycle-accurate SEQUENCE on the one-clock substrate, closing plan-residual R4 (apu_reset/4017_written) — and the investigation exposed that plan-residual R3 (apu_reset/len_ctrs_enabled) was never a core residual at all, but a test-harness protocol artifact, now fixed for every reset-protocol consumer. R4 — the $4017 reset re-write (flag-on, mc-one-clock-v2) Per the blargg apu_reset spec ("At reset ... the last value written to $4017 is written again, rather than $00" with a 9-12 clock delay before execution) and nesdev/Mesen2 ("mode is unchanged, but IRQ inhibit flag is sometimes cleared"): - FrameCounter retains the last $4017 value (`last_4017`, updated in the normal write path; power-on $00). - Apu::reset (flag-on) zeroes the sequencer + IRQ flags and SCHEDULES a re-write of `last_4017 & 0x80` (mode retained, inhibit CLEARED — retaining bit 6 wedges 4017_timing's second pass: the frame IRQ flag never sets and the ROM's 14-probe measurement cannot terminate) landing 2 clocked cycles into the CPU's 8-cycle reset delay, consumed in `tick_with_external` with the same current-parity 3/4-cycle alignment as a CPU-issued write. - Placement calibrated empirically against 4017_timing's printed "delay after effective $4017 write" (accept window 6..=12, 2-cycle APU quantization): reset-start placement reads 12 (upper edge), +3 reads 6 (lower edge), +2 lands the hardware-typical mid-window 8. - Two earlier frame-granular re-arm attempts (documented in the test file's history preamble; both reverted) failed precisely for want of the CLOCKED reset delay: this closure is only expressible on the A1/A2 substrate where every reset cycle is a real start_cycle/end_cycle. R3 — reclassified: a harness artifact, not a core residual The `run_nes_blargg_reset` protocol re-detected the STALE $81 status byte immediately after issuing a soft reset ($6000-$6003 live in WRAM, which survives reset; the ROM only rewrites them when its post-reset path reaches std_reset). Each stale re-detect re-reset the ROM mid-measurement (~every 7 frames), corrupting the second-pass log — the actual mechanism behind len_ctrs_enabled's FAIL #3, and the confound that had also masked R4's verdicts. The new stale-status guard ignores $81 after a reset until the status has read something else once (a genuine prompt always rewrites a fresh $81 after an $80 running phase). With the guard, len_ctrs_enabled passes STRICTLY ON BOTH CONFIGS — the strict test is un-ignored and its fail-loud companion deleted. Diagnosed with the NEW `reset_trace` harness bin (runs a reset-protocol ROM to its $81 prompt, resets, dumps per-frame PC/status/text), which showed both configs' second passes completing while the harness kept killing the longer flag-on measurement window. Test-pin disposition - apu_reset_len_ctrs_enabled: strict, un-ignored (passes both configs). - apu_reset_4017_written: strict flag-on (cfg_attr-ignored on default with the closure note); fail-loud companion pins the default's failing status until the beta.4 promote makes the sequence the only path. Verification (all green) - Flag-on: ALL SIX blargg apu_reset ROMs pass, zero ignored (4017_timing delay = 8, mid-window); AccuracyCoin 100% (139/139); cpu_interrupts_v2 5/5 strict; nestest 0-diff; R5 pin; counter residues (12,0,0); save-state continuation 9/9. - Flag-off: all apu_reset pass (1 ignored = the flag-gated strict); 4017_timing delay = 12 unchanged; the same core suites all green — the default build is byte-identical (the only default-path changes are the harness guard, which corrects a protocol bug, and the inert last_4017 bookkeeping). - fmt / clippy (workspace + test-roms + flag-on) / no_std flag-on / rustdoc / markdownlint clean; CHANGELOG in the same change. Remaining beta.3 scope: the R1/R2 MMC3 1-cycle-bracket hard target (bounded effort per the plan's Risks #3 escape hatch) — attempted next on this branch. Refs: ADR 0002; to-dos/plans/v2.0.0-master-clock-plan.md Workstreams A4/B; blargg apu_reset readme + 4017_timing.s/4017_written.s/len_ctrs_enabled.s; docs/audit/v2.0.0-context-brief-2026-07-01.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(v2.0.0): beta.3 — invoke the R1/R2 escape hatch (plan Risks #3) Bounded-effort disposition for the MMC3 residuals, per the plan's maintainer-pre-authorized escape hatch: with the one-clock (A1) + every-cycle-bus-access (A2) + cycle-accurate-reset (A4) substrate complete, the free-flip check ran both residual families flag-on — - R1 (mmc3_test_2/4 + mmc3_test v1/4 sub-test #3, the 1-CPU-cycle "IRQ sooner when $2000=$08" bracket): unmoved. - R2 (mmc3_test v1/5 + v1/6 sub-test #2, reload-to-0 cadence + MMC6): unmoved. Neither flips on the substrate alone: closing them requires the dedicated M2-phase sample-point campaign (the irq_trace cross-diff axis behind 17 documented rollbacks). Per Risks #3 ("do not risk a 16th rollback of the sacred 100% for the 1-cycle bracket — this is the one place 'aggressive' yields to 'sacred'") and the maintainer's confirmed threshold ("if R1/R2 don't flip cleanly within the beta.3 window without regressing the gate, accept the by-design #[ignore] and ship"), the four pins stay #[ignore]'d by design with the disposition recorded in each reason string. Zero production-ROM impact. The rc.1 ADR-0002 update will mark the axis by-design-deferred (the plan's anticipated branch). The `_currently_fails` companions remain fail-loud: if a future substrate change flips either family, the pins demand the strict flip immediately. Verification: mmc3 + apu_reset suites green in both configs (the four escape-hatched pins ignored with the new reasons; everything else strict); clippy flag-on clean; CHANGELOG updated in the same change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(v2.0.0): cancel in-flight $4017 write maturation at warm reset (review adoption) Adopt the matching gemini + copilot findings on PR #219: reset_rewrite_4017 cleared the sequencer cycle and IRQ flags but left `reset_in` armed, so a `$4017` write issued within 3-4 cycles of the reset button would mature DURING the reset sequence and race the scheduled reset re-write. `reset_in` is now cancelled in the same zeroing block. Re-gated: all six blargg apu_reset ROMs green flag-on (zero ignored) and flag-off (1 flag-gated ignore); clippy flag-on clean; fmt clean. Also adjudicated + resolved on the PR (no code change): the reset_trace expect() style (house probe-bin convention) and the "should should" quote (blargg's own ROM string, quoted verbatim deliberately). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…osure campaign (#221) * feat(mappers): Vs. DualSystem board support in mapper 99 Teach the Vs. System board (iNES mapper 99) the three hardware facts a DualSystem cabinet adds on top of a UniSystem cart, behind default-no-op Mapper trait hooks so every other board (and every single-console mapper-99 boot) is byte-identical: - Shared 2 KiB work RAM at $6000-$7FFF, mirrored across the 8 KiB window (MAME vsnes.cpp: `map(0x6000, 0x67ff).mirror(0x1800).ram()`). Each console's mapper instance holds a COPY plus a write log (`take_vs_dual_wram_writes` / `apply_vs_dual_wram_write`); the VsDualSystem wrapper converges the copies after every stepped instruction. This realizes MAME's fully-shared `.share("nvram")` memory model at soft-lockstep granularity WITHOUT aliasing (no Rc/RefCell in the no_std chip stack). The model choice is evidence-driven: nesdev/Mesen2 document a $4016-bit-1 access mux (exclusive ownership), but Balloon Fight's boot handshake polls a shared-WRAM mailbox ($6220) while the mux would deny its partner access — under exclusive routing the boot provably deadlocks, and MAME (where the four DualSystem games verifiably run) shares the RAM unconditionally. - Sub-console PRG/CHR banking (`set_vs_dual_sub`): the cabinet's two CPUs run DIFFERENT programs. MAME `balonfgt` loads distinct ROMs into its `prg` and `sub` regions (the sub's 6d/6a chips differ from the main's 1d/1a by CRC); Mesen2 banks `prgOuter = main ? 0 : 4` in 8 KiB pages. The sub instance offsets PRG reads by 32 KiB and ORs CHR page bit 1, both modulo the ROM size — so a 32 KiB (main-half-only) dump wraps onto the same program and a proper 64 KiB dual dump splits. - A versioned save-state: UniSystem carts keep emitting the v1 layout byte-identically; a provisioned dual WRAM bumps to v2 (v1 + the 2 KiB tail). The write log is transient (always drained within the stepping loop) and deliberately not serialized; the sub identity is cabinet wiring re-applied by the wrapper, like the bus's $4016 bit-7 flag. Part of v2.0.0 beta.5 (plan Workstream C, ADR 0002). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(core): Vs. DualSystem bus comms surface + instruction stepping Give LockstepBus the cabinet-side half of the DualSystem cross-wiring, strictly following the design rule from docs/audit/vs-dualsystem-design-2026-06-11.md: the two consoles' buses NEVER reference each other — each bus only reports its own signals and accepts externally-driven levels, and the VsDualSystem wrapper owns all routing. Everything is dead on a single console (two field writes on the $4016 store, an OR with an always-false flag in irq_level), so the deterministic single-console path is behavior-identical — AccuracyCoin holds 139/139 and nestest stays 0-diff. - $4016 write: latch the bit-1 (main/sub comms) LEVEL on EVERY write for the wrapper to poll (`take_vs_mainsub_edge`, poll-and-clear). Deliberately not edge-filtered: the wrapper seeds the reset-time levels itself (Mesen2 `VsControlManager::Reset` seeds main LOW / sub HIGH), so a bus-side edge filter starting from a false latch would swallow the genuine seeded-HIGH -> written-LOW transition Balloon Fight's reset performs ($8009: STA $4016 with $00 on both CPUs) and deadlock the boot handshake. Applying an unchanged level is idempotent in the wrapper. - $4016 read overlay: bit 7 = sub-console identity (`set_vs_sub`; main reads 0, sub reads $80 — MAME `ret |= Side << 7`, Mesen2 `IsVsMainConsole() ? 0x00 : 0x80`). This is how the shared program ROM decides which half it is. - External /IRQ: `set_vs_external_irq` drives a level the partner's bit-1 write controls (LOW asserts, HIGH releases — MAME `set_input_line(0, (data & 2) ? CLEAR_LINE : ASSERT_LINE)`); OR'd into `irq_level()` alongside the mapper and APU lines. - Passthroughs for the mapper-99 dual-WRAM hooks (enable / write-log drain+replay / take+set / sub banking), keeping the mapper field encapsulated. - `Nes::is_jammed()` + `Nes::step_instruction()`: the wrapper's soft-lockstep steps each console one INSTRUCTION at a time (Mesen2 `RunFrame` + `RunVsSubConsole`), so the debugger-oriented single-step needed a public, jam-guarded form that leaves the frame-complete latch for the wrapper to consume. - vs_db: the `dual_system` flag's doc now reflects that Emu::from_rom routes flagged carts to the full two-console wrapper (it was the "show a needs-support note" placeholder from v2.7.1); the SHA-keyed db is the load-bearing detection source because the circulating DualSystem dumps are iNES 1.0 with no NES 2.0 byte-13 hardware type. Part of v2.0.0 beta.5 (plan Workstream C, ADR 0002). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(core): VsDualSystem wrapper + the Emu construction front door Add rustynes-core::vs_dualsystem — the Vs. DualSystem cabinet as a wrapper over two complete Nes instances, plus the `Emu` enum frontends construct through (`Single(Box<Nes>)` / `Dual(Box<VsDualSystem>)`). Architecture (docs/audit/vs-dualsystem-design-2026-06-11.md): the wrapper owns ALL cross-wiring; the two consoles never hold references to each other. After every stepped instruction, `pump_comms` drains each bus's $4016 bit-1 level (driving the partner's external /IRQ — LOW asserts, per Mesen2 `UpdateMainSubBit` / MAME `(data & 2) ? CLEAR_LINE : ASSERT_LINE`) and each mapper's shared-WRAM write log (replaying it into the partner's copy — MAME's fully-shared `.share("nvram")` model; see the mapper commit for why the nesdev/Mesen2 exclusive-access mux provably deadlocks Balloon Fight's boot and is not used). Stepping mirrors Mesen2 `NesConsole::RunFrame` + `RunVsSubConsole`: the main console steps one instruction, then the sub drains until it is within a 5-CPU-cycle gap (or has caught the main's frame). The gap comparison is overshoot-safe (`main > sub.saturating_add(5)`): an instruction advances 2..=8 cycles, so the sub routinely lands AHEAD of the main — a naive `wrapping_sub(..) > 5` wraps to a huge unsigned value there and runs the sub away unboundedly (the first implementation of this loop did exactly that: ~68 CPU-minutes and 5.5 GiB of undrained audio before diagnosis). Reset-time seed per Mesen2 `VsControlManager::Reset` (`UpdateMainSubBit(main ? 0x00 : 0x02)`): the main boots bit-1 LOW (sub /IRQ asserted), the sub HIGH (main /IRQ clear) — Wrecking Crew's handshake requires it. Cabinet routing: controller ports 0/1 -> main, 2/3 -> sub P1/P2; coin acceptors 0/1 -> main, 2/3 -> sub; per-panel service buttons; per-console DIP banks fall out structurally (two buses, two vs_dip bytes — Mesen2's `dipSwitches >> 8`). Save states: an `RVSD` + u16-version container nesting the two u32-length-prefixed Nes snapshots plus the wrapper's bit-1 latch byte. Restore re-drives the cross-IRQ levels from the latch and re-converges the two shared-WRAM copies from one buffer (main's copy is authoritative), so a cross-restore can never leave the cabinet with diverged RAMs. `Emu::from_rom` detection ORs the NES 2.0 byte-13 hardware type (5/6 = DualSystem) with the SHA-keyed vs_db `dual_system` flag; the db is load-bearing because the circulating dumps are iNES 1.0. Out of scope by design (plan + design doc): netplay rollback and RetroAchievements do not support the dual path; the frontend drains the main console's mixer only. Part of v2.0.0 beta.5 (plan Workstream C, ADR 0002). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(harness): Vs. DualSystem validation suites + handshake trace probe Three-layer verification for the beta.5 dual-console work, honest about what each layer can and cannot prove: - tests/vs_dualsystem_synth.rs (CI-committable, no external assets): builds a synthetic NES 2.0 DualSystem cart in-code — 64 KiB PRG carrying two DIFFERENT hand-assembled 6502 programs (byte-13 hardware type 5) — and proves every wire of the cabinet model end-to-end: header-based dual detection through Emu::from_rom, sub-console second-PRG-half banking (the sub's identity check fail-marks if it ran the main's program), $4016 bit-7 identity (main 0 / sub $80), shared-WRAM convergence (an $11/$22 mailbox exchange visible from both sides), the cross-IRQ protocol in both directions (main asserts the sub's /IRQ via bit-1 LOW; the sub's handler answers through WRAM and pulses the main back), and the RVSD snapshot round-trip continuing cycle-identically. 3/3 green. - tests/vs_dualsystem.rs (commercial-roms gated): boots the four staged GVS dumps through Emu::from_rom. The snapshot round-trip passes; the four boot tests are #[ignore]d WITH the evidence in the reason — the circulating 32 KiB GVS dumps are the MAME maincpu region ONLY (byte-for-byte: GVS Balloon Fight's PRG chunks CRC32-match balonfgt's mds-bf4 a-3.1d/1c/1b/1a; the cabinet's sub CPU runs the different .6d/.6a ROMs, absent from the dumps; GVS Tennis matches vstennisa's main region the same way). The main program's boot handshake waits forever on a sub-side answer ($AA at shared-WRAM offset $220) only the missing sub program can write, so these dumps cannot boot dual on ANY emulator. Re-enable when combined 64 KiB dual dumps are staged. - src/bin/vs_dual_trace (diagnostic): boots a dump through VsDualSystem, prints per-console PC histograms, framebuffer-colour checkpoints, shared-mailbox samples, and a dense two-CPU instruction trace with every comms level + WRAM write annotated. This is the tool that localized the boot deadlock to the $81CF mailbox exchange and disproved the exclusive-WRAM model (both CPUs polling $6220 for a value with no writer anywhere in the 32 KiB binary). Part of v2.0.0 beta.5 (plan Workstream C, ADR 0002). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(v2.0.0): re-baseline the mmc3 irq_trace goldens to the promoted core The R1/R2 closure campaign (the bounded effort the beta.3 escape hatch deferred, run on the fully promoted one-clock/every-cycle core) regenerated the mmc3_test_2/4-scanline_timing irq_trace goldens: the committed set was captured on the pre-promote core and no longer reflects the shipping timeline. The fresh capture (fixture green) records the mapper irq_pending asserts at (frame 43, scanline 0, dot 260) / (frame 71, 0, 261) with services at dots 279/280 — the ground truth for the campaign's analysis. Campaign outcome (full record: docs/audit/r1r2-closure-campaign-2026-07-02.md, local): both structural hypotheses FALSIFIED with clean gates — (1) the sprite-fetch A12 emission-dot shift (260->259) is absorbed by CPU-cycle batch quantization; (2) Mesen2's do-while catch-up boundary semantics (the exact-boundary dot executing in the current batch — a real structural difference vs our check-first run_ppu_to) held AccuracyCoin 139/139 + the C1 trio + nestest AND left all four target brackets unchanged. Mechanism identified: the blargg bracket measures the interval between two same-timeline observations ($2002 VBL read -> IRQ window), so ANY consistent batch re-phasing shifts both legs together and cancels — the residual is differential and unreachable on any 3-dots-per-cycle-batched catch-up model. The next credible attempt is the per-dot interleaved scheduler (the documented post-v2.0.0 axis). The four pins stay by-design #[ignore]'d; the two falsified levers join the DO-NOT-RETRY list via the rc.1 ADR-0002 update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(vs-dualsystem): verify Balloon Fight boots on a combined 64 KiB dump The four Vs. DualSystem boards (Tennis / Mahjong / Wrecking Crew / Balloon Fight) run two complete NES consoles sharing a comms latch and a 2 KiB WRAM mailbox. Every previously-staged "GVS" dump at tests/roms/external/vs-system/ is 32 KiB of PRG -- the MAME `maincpu` region only. VsSystem::cpu_read's dual_sub branch banks the SUB console's PRG from offset 0x8000 into the cart's PRG array, so with only 32 KiB present the sub console silently re-runs a wrapped copy of the MAIN program instead of its own code, and the boot handshake -- which polls for a sub-side mailbox answer -- deadlocks instantly (flat, single-colour framebuffer on both sides, as previously documented in the four #[ignore]'d tests in vs_dualsystem.rs). Two of the four titles' missing sub-CPU program ROMs were located in a legitimately-owned MAME arcade romset (balonfgt.zip / wrecking.zip; Tennis and Mahjong were not present in that set and remain out of scope). A combined 64 KiB-PRG dump was assembled locally for each (gitignored under tests/roms/external/, never committed) by concatenating the existing 32 KiB main-half PRG with the sub-CPU PRG chunks extracted from the zip, in the same empirically-confirmed d,c,b,a chunk order the main half already uses, plus the shared CHR verbatim. Full assembly method, CRC32/SHA-256 verification chain, and boot-outcome writeup: docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md (local-only per this repo's docs/audit/ convention -- see .gitignore). Findings, verified via crates/rustynes-test-harness/src/bin/vs_dual_trace.rs and the new tests below: - Balloon Fight boots for real on the combined dump. The handshake completes (neither CPU jams over 1200 frames / 20 simulated seconds), and both consoles render an identical, legible attract-mode menu ("1PLAYER VS. COMPUTER" / "2PLAYERS MUST USE BOTH SCREENS" / a credit counter that exactly tracks the 10 simulated coin pulses the harness performs). This is qualitatively different from the 32 KiB dump's total deadlock, not just "less broken." - Wrecking Crew is inconclusive. The wrapper's cross-wiring is demonstrably active (bidirectional $4016 bit-1 /IRQ toggling and shared-WRAM writes observed in a dense instruction trace -- a real improvement over instant deadlock), but the framebuffer never exceeds 3 distinct colours and oscillates on a stable ~600-frame period whether or not simulated coins are inserted (re-verified with coin injection disabled), so simulated-input timing is ruled out as the blocker. This does not distinguish a missing input sequence from a residual cross-wiring bug specific to this title's handshake shape, and per this project's testing discipline the corresponding test stays #[ignore]'d rather than asserting a false positive. vs_dualsystem.rs changes: - New passing test `gvs_balloon_fight_dual_combined_boots`, which deliberately does NOT reuse the existing `assert_dual_alive` helper: that helper's `> 4 distinct colours` heuristic and `main != sub` framebuffer check are tuned for typical multi-colour commercial titles with diverged player views, both of which this legitimate two-colour, pre-divergence attract screen violates by design. Instead it asserts the CPUs aren't jammed and pins an `insta` hash-based text snapshot (cycle counts, distinct-colour counts, FNV-1a64 framebuffer hashes) following this crate's existing external_coverage.rs convention, after visually confirming the dumped PNGs show real, legible game text. - New `#[ignore]`d diagnostic `diag_gvs_wrecking_crew_dual_combined`, asserting only the confirmed-true fact (neither CPU jams) with a reason string pointing at the audit doc for the next investigation session. - Module docs updated with the 2026-07-02 finding; the original four 32 KiB-only boot tests are untouched (still #[ignore]'d, still expected-fail on those specific dumps). vs_db.rs changes: - Two new `entry_dual(...)` SHA-256 rows for the combined dumps (Balloon Fight vs_dip=0x00/Rp2C04_0003, Wrecking Crew vs_dip=0xF8/Rp2C04_0002 -- identical DIP/PPU values to the existing 32 KiB-only entries for the same games, since those describe the cabinet, not the dump completeness), inserted at the correct sorted position (enforced by db_is_sorted_by_sha256). The pre-existing 32 KiB-only entries are untouched and still resolve correctly; loading that specific incomplete dump still flags dual_system (still routes to VsDualSystem) and simply can't complete the handshake, which remains expected/harmless. - `exactly_the_four_dualsystem_carts_are_flagged` renamed to `exactly_six_dualsystem_rows_across_the_four_carts_are_flagged` and its assertion updated from 4 to 6: still exactly 4 DualSystem GAMES, but 2 of them now have both an incomplete and a complete dump row. No changes to crates/rustynes-core/src/vs_dualsystem.rs (the wrapper's cross-wiring logic) -- both the Balloon Fight success and the Wrecking Crew partial result were produced by the existing model against newly supplied ROM data; no bug was found or fixed in the wrapper itself during this investigation. Vs. Tennis and Vs. Mahjong are completely untouched. Verification: cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings (plus the scripting / hd-pack / retroachievements frontend feature combos), cargo test --workspace (full dev-profile run, zero failures), and targeted re-runs of crates/rustynes-test-harness/tests/vs_dualsystem.rs (2 passed / 5 ignored) and vs_dualsystem_synth.rs (3/3, confirming the synthetic-cart protocol test that CI actually gates on is untouched) under --features commercial-roms,test-roms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(v2.0.0): R1/R2 closure attempt — M2-phase-conditional MMC3 IRQ visibility (default-off, falsified) The maintainer asked for one more bounded attempt at the R1/R2 residual (the MMC3 1-CPU-cycle IRQ-timing bracket in mmc3_test_2/4-scanline_timing sub-test #3 and its mmc3_test_v1/{4,5,6} siblings), chartered as implementing a "genuine per-dot interleaved CPU/PPU scheduler" per Session-18's Mesen2 StartCpuCycle/Read/EndCpuCycle comparison in ADR-0002. Investigation showed that model already shipped: the v2.0.0 beta.1-beta.4 promote made Cpu::start_cycle -> access -> Cpu::end_cycle (each half calling Bus::run_ppu_to, which ticks the PPU one whole dot per iteration) the ONLY scheduler path, unconditionally. There is no remaining coarse-batch axis to split further. What this commit actually adds is the one concrete, previously-untested lever that remained: real M2-phase (pre-access/low vs post-access/high) visibility for MMC3's IRQ-pending-line assertion, gated behind two paired default-off features (rustynes-core/mmc3-m2-phase-irq, rustynes-mappers/mmc3-m2-phase-irq, forwarded through rustynes-test-harness). Instrumentation this session found the M2-phase plumbing ADR-0002 describes (Mapper::notify_a12_at_sub_dot's "sub-dot 0/1=low, 2=high" convention) was never actually wired to carry real phase data on the live R1 scheduler path -- LockstepBus::run_ppu_to constructed its PpuBusAdapter from a call-LOCAL sub_dot counter that resets to 0 on every invocation, and since run_ppu_to is called twice per CPU cycle (once per half) with each half typically ticking at most one dot, the value threaded to the mapper was almost always 0 regardless of which half produced the transition. Bus::run_ppu_to gained an is_post_access: bool parameter (threaded from the two call sites in Cpu::start_cycle/end_cycle); LockstepBus::run_ppu_to now seeds sub_dot from that real phase under the feature (0 pre-access, 2 post-access, matching the documented convention) instead of the always-reset counter. Mmc3::notify_a12_at_sub_dot uses this to defer a qualifying A12 rise's irq_pending_line assertion by exactly one notify_cpu_cycle boundary when the rise lands post-access (M2-high), while asserting synchronously for pre-access (M2-low) rises -- modeling the propagation-delay asymmetry ADR-0002 hypothesizes. An $E000 ack/disable write cancels an in-flight deferred assertion. This is structurally distinct from every one of the 17+ prior rolled-back attempts (constant-cycle pipelines, gap-threshold tuning, global batch-boundary re-phasings) -- it is a per-rise-property- conditional visibility deferral, evaluated on its own merits against today's promoted core rather than re-deriving a documented dead end. Three new unit tests in rustynes-mappers::mmc3::tests prove the deferral mechanism itself works correctly in isolation (M2-high defers one cycle, M2-low asserts synchronously, ack cancels a pending deferral). But regenerating the irq_trace_fixture for mmc3_test_2/4-scanline_timing with the feature ON vs OFF produces a byte-for-byte identical run (83 frames, 2,203,768 trace records, final $6000=$03) in both configurations -- meaning no qualifying A12 rise this ROM's actual execution produces ever lands during the post-access half of a CPU cycle under the current scheduler, so the phase-conditional lever has zero differential effect on this specific bracket. The two *_currently_fails fail-loud probes (mmc3_test_2/4, mmc3_test_v1/4) both still correctly detect the unmoved failure with the feature on. This is a clean, mechanism-verified falsification, not a regression -- the four target brackets remain by-design #[ignore]'d, unchanged. The code is kept (not reverted) because it is fully feature-gated, confirmed byte-identical to the pre-attempt default build (fmt/clippy/ AccuracyCoin 139/139/cpu_interrupts_v2 5/5/nestest 0-diff/the R5 DMC-DMA pin/mmc3 18-pass-5-ignored/one_clock_invariants 2/2/save_state 9/9 all held with the feature off), and fixes a genuine ADR-0002 documentation gap (real M2-phase data was never actually reachable from the live R1 path before this commit) -- useful infrastructure for whoever tests the gap-accounting M2-edge-precision axis the same-day closure campaign flagged as the next credible lever. Full record, evidence, and the new falsifiable hypothesis for the next attempt in docs/audit/r1r2-per-dot-scheduler-attempt-2026-07-02.md (gitignored, local to this worktree per project convention). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(v2.0.0): beta.5 CHANGELOG entry + ADR-0002 rc.1 disposition update Consolidates the two 2026-07-02 R1/R2 bounded-effort campaigns (Session A: batch-boundary re-phasing, both experiments falsified; Session B: real M2-phase-conditional MMC3 IRQ visibility, mechanism-verified falsification plus a fixed dead-plumbing bug) into a single ADR-0002 decision-update section, per the maintainer's explicit direction to fold both findings into one entry rather than two. Marks the C1/MMC3 axis by-design-deferred beyond v2.0.0 with 21+ documented rollbacks (17 historical + 4 new), records the four new DO-NOT-RETRY levers, and flags the one genuinely untested axis (falling-edge gap>=3 low-time accounting) for a future dedicated session rather than continued spend within this release. CHANGELOG's [Unreleased] section gains the beta.5 entry: the Vs. DualSystem dual-console feature (mapper 99 board support, the VsDualSystem wrapper's shared-WRAM/cross-IRQ model, the Emu construction front door, synth-proven protocol tests) plus the real commercial-boot result (Vs. Balloon Fight verified booting on a combined dump assembled from a legitimately-owned MAME romset; Vs. Wrecking Crew documented honestly as inconclusive rather than forced to a false-positive pass) and a summary of the R1/R2 campaign disposition cross-referencing the ADR update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(vs-dualsystem): address gemini/Copilot PR #221 review findings Six findings from the initial bot review pass, all adopted: - **Hot-path allocation (gemini high, Copilot)**: `pump_comms` called `Mapper::take_vs_dual_wram_writes`, which used `mem::take` to hand the caller a fresh Vec every call -- after every stepped instruction on a DualSystem cart, this is a real hot-path allocation, not a theoretical one. Replaced with a new `Mapper::drain_vs_dual_wram_writes(&mut self, dst: &mut Vec<(u16, u8)>)` primitive: `VsSystem`'s override uses `Vec::append` (clippy's own preferred form over `extend(drain(..))`), which empties `dual_wram_log` while retaining ITS capacity; the wrapper gained a reusable `comms_scratch: Vec<(u16, u8)>` field so its side of the exchange is allocation-free too, once warmed up. The old `take_vs_dual_wram_writes` stays as a convenience default method (built on the new primitive) for non-hot-path callers (`vs_dual_trace.rs`, tests) -- unchanged call sites there. - **Save-state restore leaves stale WRAM-log entries (Copilot)**: `VsSystem::load_state` never cleared the transient `dual_wram_log`, so writes logged before a restore point would replay into the partner console AFTER the restore, corrupting its shared-WRAM copy with pre-restore data. Now cleared unconditionally on every restore. Also: a v1 (UniSystem) load no longer leaves a stale `dual_wram` allocation behind if the live instance had previously been dual-provisioned -- it's explicitly dropped to match the versioned layout just loaded. - **Two doc/implementation mismatches (Copilot)**: `vs_4016_bit1_dirty`'s field doc and `take_vs_mainsub_edge`'s doc both described an edge- filtered ("only when bit 1 changed") protocol; the actual, intentional design is level-driven (dirty on EVERY `$4016` write -- required so the reset-time seeded levels aren't swallowed by an edge filter starting from a false latch, per the existing `cpu_write` comment). Rewrote both doc comments to describe the real, level-driven semantics. - **Frontend-integration overclaim (Copilot)**: the `Emu` module doc and the original PR description both said "frontends construct via Emu::from_rom" -- but `rustynes-frontend` still constructs `Nes` directly everywhere (`app.rs`, `emu.rs`) and does not consume `Emu` at all, so the DualSystem path is unreachable from the shipped desktop/ mobile UI in this release. Corrected the module doc and the CHANGELOG entry to state this plainly as a known, explicitly deferred gap rather than implying frontend wiring already landed. - **Non-portable hardcoded /tmp path (gemini medium)**: the test harness's best-effort PNG dump helper hardcoded `/tmp/RustyNES/vs-dualsystem`, which doesn't exist on Windows. Switched to `std::env::temp_dir()`; dropped the now-unused `Path` import (`PathBuf` still used elsewhere in the file). Verified: fmt clean; clippy clean across workspace + the 3 frontend feature combos; full dev-profile `cargo test --workspace` 111/111 green; release-profile `vs_dualsystem` (2 passed/5 ignored, `mmc3` (18/5-ignored), and `vs_dualsystem_synth` (3/3) all still pass identically to before these fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(vs-dualsystem): drop private intra-doc link breaking the rustdoc CI gate The prior commit's take_vs_mainsub_edge doc fix linked to [`Self::vs_4016_bit1_dirty`] -- a private field -- from a pub method's doc comment. rustdoc's -D warnings gate (RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps, the exact CI command) correctly flags public-documentation-links-to-private-item as an error. Swapped the intra-doc link for a plain code span; verified locally with the exact CI invocation plus fmt/clippy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…(Fathom F0) (#253) * docs(status): reconcile STATUS.md accuracy residuals with the ledger (F0) The "## Accuracy residuals" section + the Test-ROM table's AccuracyCoin / cpu_interrupts_v2 / mmc3 rows + the #[ignore] top-line count were stale engine-lineage text contradicting docs/accuracy-ledger.md (the authoritative scoreboard). Reconcile the CURRENT-state claims: - AccuracyCoin: 98.58% (139/141) -> 100.00% (141/141). The two v2.0.1-added PPU tests ("ALE + Read", "Hybrid Addresses") were closed in v2.0.3 by promoting the 2-cycle-ALE fetch model to the default; no open gaps remain. - cpu_interrupts_v2: was shown 2/5 default + 3 #[ignore]; now 5/5 strict on the default master-clock build (the C1 residual is closed). - MMC3 R1/R2 scanline-IRQ residual (mmc3_test_2/4 #3 + siblings): the stale "keep deferring, pending a fractional-master-clock refactor" verdict is superseded by v2.1.0 "Fathom" F5.0 / ADR 0002 -> CLOSED by-design-permanent (a differential 1-dot deficit structurally unreachable on the one-clock scheduler, 21+ falsified levers, zero production-ROM impact). Updated the residuals section, the mmc3_test_2 test row, and the mapper-table MMC3 row. - #[ignore] count: 5 -> the authoritative 20-test catalogue (9 by-design + 4 historical mock-bus pins + 5 external-fixture-blocked Vs. DualSystem boots + 2 CI-hermetic network/HD-pack), all cross-referenced to the ledger; none is an open accuracy gap. Historical v2.0.1/v2.0.2 blockquotes that state 139/141 are left intact — they are accurate provenance for those releases (pre-v2.0.3). Docs-only; no code, no behavior, no oracle move. markdownlint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(status): align #[ignore] grouping with the accuracy-ledger table Address the PR #253 review: the STATUS.md #[ignore] breakdown grouped the 20 ignored tests as 9 by-design / 4 historical / 5 fixture / 2 CI-hermetic, which did not match the authoritative categorization in docs/accuracy-ledger.md. Re-group to the ledger's exact four-way split so the two docs cannot drift: 7 permanent historical pins (APU $4015/reload-arm/ put_cycle + 3 CPU interrupt-dispatch + PPU BG-shifter mock-bus assertions, superseded by AccuracyCoin 100% / cpu_interrupts_v2 5/5 / visual_regression 7/7), 5 by-design (4 MMC3 R1/R2 scanline-IRQ brackets closed per ADR 0002 + the NEC-rev-B mmc3_test_2/6-MMC3_alt opposite-silicon-revision pin), 5 Vs. DualSystem fixture-blocked GVS boots (need a combined dual-CPU dump), and 3 CI/fixture (stun/turn live-network probes + the hdpack copyrighted-pack test). Total unchanged at 20; none is an open accuracy gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a purely-observational, default-off `mmc3-a12-phase-probe` cargo feature (threaded `rustynes-mappers` -> `rustynes-core` -> `rustynes-test-harness`) plus a reproducible study fixture that answers, with fresh direct instrumentation, the one avenue ADR 0002's v2.1.0 F5.0 closure left explicitly open: on the four `#[ignore]`'d MMC3 IRQ residuals, does any *qualifying* (`gap >= 3`) A12 rising edge that clocks the IRQ counter ever land in the post-access (M2-high, phi2) half of a host CPU cycle -- the sub-cycle window an ares-style M2-half-cycle low-time filter would treat differently from the integer `gap >= 3` model? Mechanism (byte-identical to the default build): - `LockstepBus::run_ppu_to` already seeds the real M2-phase into the mapper `sub_dot` (pre-access/M2-low = 0, post-access/M2-high = 2) under the existing `mmc3-m2-phase-irq` lever. The new feature reuses that exact seeding via an `any(...)` cfg so the probe observes real per-dot phase on the promoted one-clock scheduler. - In `Mmc3::notify_a12_at_sub_dot` the original `if gap >= 3 && self.clock_irq()` is refactored into a nested form so the probe can observe a *qualifying* rise (gap accepted) independently of whether it went on to clock the counter. The refactor is behavior-identical: `clock_irq()` (which mutates) is still only evaluated when `gap >= 3`, exactly the short-circuit the original `&&` gave. - The probe ONLY increments four `u64` counters (qual_pre/qual_post, irq_pre/irq_post) -- it never touches `irq_pending_line` or any emulated state, so with the feature ON the timeline is byte-identical to the default. Tallies are surfaced via `MapperDebugInfo.extra` and read back by the fixture without adding trait methods. The entire probe compiles out when the feature is off (zero footprint on the shipped / `#![no_std]` build). Finding -- the "no post-access qualifying rise" premise is ROM-specific: - `mmc3_test_2/4` #3 and `mmc3_test_v1/4` #3 (scanline_timing): irq_post = 0, directly confirming Session B's (2026-07-02) indirect byte-identity result -- for these, axis B is dead. - `mmc3_test_v1/5` #2 and `/6` #2 (reload/set-IRQ-every-clock), which Session B never measured, produce 4 post-access IRQ-clocking rises each (every qualifying rise is post-access). So "no post-access rise" is a property of the scanline-timing ROMs' phase alignment, not a structural NTSC-MMC3 invariant. - Engaging the existing `mmc3-m2-phase-irq` rising-edge deferral on `/5`+`/6` leaves their failure status byte-identical -- that concrete lever is non-curative and stays on the DO-NOT-RETRY list. Disposition: the F5.0 by-design closure HOLDS for the shipping default and for every lever ever tested; its rationale shifts from "no post-access rise is structurally possible" to "no tested lever is curative." One genuinely-untested axis-B lever -- an ares-style M2-edge-precise falling-edge low-time filter, which needs a sacred-gate-risking scheduler-substrate change even to prototype -- is recorded in ADR 0002's 2026-07-11 decision update as a candidate deferred to a maintainer decision. The four residuals stay `#[ignore]`'d with their fail-loud `*_currently_fails` companions; AccuracyCoin stays 141/141 and the default build is byte-identical (no production, scheduler, or MMC3 default behavior changed). Docs: ADR 0002 gains the 2026-07-11 v2.1.5 F5.0 decision update (method, the per-ROM tally table, and the deferred-lever disposition); `docs/accuracy-ledger.md` is updated to the refined disposition; the CHANGELOG `[Unreleased]` records the probe + finding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 pal_apu_tests Implement the PAL (2A07) APU frame-counter sequencer step positions, closing five of the seven PAL timing residuals the salvaged screen-reading oracle pinned. The blargg `pal_apu_tests` corpus now scores 8/10 under forced PAL region (was 3/10), with the two remaining failures documented as a bounded, fail-loud residual. Mechanism --------- The 2A03 (NTSC) and 2A07 (PAL) share the same six-step sequencer but divide the CPU clock differently, so the identical quarter/half/IRQ events land at different CPU-cycle counts. `FrameCounter` now carries a `pal: bool` selector, derived from the console `Region` by `Apu::new` (true only for `Region::Pal`; NTSC and Dendy — a PAL-clocked famiclone with an NTSC-period frame counter — keep the NTSC positions). The sequencer step tables are: - PAL 4-step (mode 0): 8313 / 16627 / 24939 / 33252 / 33253 / 33254 (quarter 8313/16627/24939/33253; half 16627/33253; IRQ 33252/33253/33254). - PAL 5-step (mode 1): 8313 / 16627 / 24939 / 41565 / 41566 (quarter 8313/16627/24939/41565; half 16627/41565; no IRQ). These are the canonical Mesen2 `stepCyclesPal` values. The mode-0 terminal three cycles replicate the NTSC IRQ-flag-visibility / `irq_line_active` split verbatim at the PAL positions, preserving the AccuracyCoin Frame-Counter-IRQ Tests I/J/K/L/M semantics for PAL. NTSC byte-identity (sacred constraint) -------------------------------------- The change is strictly region-gated: the `pal == false` branch runs the original NTSC step tables unchanged, and the power-on / snapshot-restore default is NTSC. The `tick` hot path was split into `clock_sequencer` + `four_step` / `five_step` helpers (to stay within the clippy line budget) with no behavioral change to the NTSC arms. Verified byte-identical: - AccuracyCoin 141/141 (100.00%) — unchanged. - `apu_test` 8/8, NTSC `blargg_apu_2005` 11/11, `nestest` 0-diff — unchanged. - APU snapshot format is untouched: `pal` is derived, not persisted; `Apu::restore` re-derives it from the restored region. Residual (honest) ----------------- `10.len_halt_timing` and `11.len_reload_timing` still fail, but the PAL step positions advanced them from `FAILED: #2` to `FAILED: #3` / `#4`. Their NTSC builds pass (`blargg_apu_2005` 10 & 11), localizing the gap to a PAL-specific length-counter halt/reload write-vs-half-frame-clock ordering detail adjacent to the frame-counter step model. Left as fail-loud residual pins and recorded in docs/accuracy-ledger.md; closing it is a separate deeper investigation. Tests + docs ------------ Adds seven PAL frame-counter unit tests (step positions, IRQ position, no-IRQ-at-NTSC-position, wrap, 5-step, NTSC-default). Promotes pal_apu 04-08 from residual pins to strict passes; keeps 10/11 as documented residuals. Updates docs/apu-2a03.md, docs/accuracy-ledger.md, docs/STATUS.md, docs/testing-strategy.md to the 8/10 state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 pal_apu_tests Implement the PAL (2A07) APU frame-counter sequencer step positions, closing five of the seven PAL timing residuals the salvaged screen-reading oracle pinned. The blargg `pal_apu_tests` corpus now scores 8/10 under forced PAL region (was 3/10), with the two remaining failures documented as a bounded, fail-loud residual. Mechanism --------- The 2A03 (NTSC) and 2A07 (PAL) share the same six-step sequencer but divide the CPU clock differently, so the identical quarter/half/IRQ events land at different CPU-cycle counts. `FrameCounter` now carries a `pal: bool` selector, derived from the console `Region` by `Apu::new` (true only for `Region::Pal`; NTSC and Dendy — a PAL-clocked famiclone with an NTSC-period frame counter — keep the NTSC positions). The sequencer step tables are: - PAL 4-step (mode 0): 8313 / 16627 / 24939 / 33252 / 33253 / 33254 (quarter 8313/16627/24939/33253; half 16627/33253; IRQ 33252/33253/33254). - PAL 5-step (mode 1): 8313 / 16627 / 24939 / 41565 / 41566 (quarter 8313/16627/24939/41565; half 16627/41565; no IRQ). These are the canonical Mesen2 `stepCyclesPal` values. The mode-0 terminal three cycles replicate the NTSC IRQ-flag-visibility / `irq_line_active` split verbatim at the PAL positions, preserving the AccuracyCoin Frame-Counter-IRQ Tests I/J/K/L/M semantics for PAL. NTSC byte-identity (sacred constraint) -------------------------------------- The change is strictly region-gated: the `pal == false` branch runs the original NTSC step tables unchanged, and the power-on / snapshot-restore default is NTSC. The `tick` hot path was split into `clock_sequencer` + `four_step` / `five_step` helpers (to stay within the clippy line budget) with no behavioral change to the NTSC arms. Verified byte-identical: - AccuracyCoin 141/141 (100.00%) — unchanged. - `apu_test` 8/8, NTSC `blargg_apu_2005` 11/11, `nestest` 0-diff — unchanged. - APU snapshot format is untouched: `pal` is derived, not persisted; `Apu::restore` re-derives it from the restored region. Residual (honest) ----------------- `10.len_halt_timing` and `11.len_reload_timing` still fail, but the PAL step positions advanced them from `FAILED: #2` to `FAILED: #3` / `#4`. Their NTSC builds pass (`blargg_apu_2005` 10 & 11), localizing the gap to a PAL-specific length-counter halt/reload write-vs-half-frame-clock ordering detail adjacent to the frame-counter step model. Left as fail-loud residual pins and recorded in docs/accuracy-ledger.md; closing it is a separate deeper investigation. Tests + docs ------------ Adds seven PAL frame-counter unit tests (step positions, IRQ position, no-IRQ-at-NTSC-position, wrap, 5-step, NTSC-default). Promotes pal_apu 04-08 from residual pins to strict passes; keeps 10/11 as documented residuals. Updates docs/apu-2a03.md, docs/accuracy-ledger.md, docs/STATUS.md, docs/testing-strategy.md to the 8/10 state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0) (#274) Adds a purely-observational, default-off `mmc3-a12-phase-probe` cargo feature (threaded `rustynes-mappers` -> `rustynes-core` -> `rustynes-test-harness`) plus a reproducible study fixture that answers, with fresh direct instrumentation, the one avenue ADR 0002's v2.1.0 F5.0 closure left explicitly open: on the four `#[ignore]`'d MMC3 IRQ residuals, does any *qualifying* (`gap >= 3`) A12 rising edge that clocks the IRQ counter ever land in the post-access (M2-high, phi2) half of a host CPU cycle -- the sub-cycle window an ares-style M2-half-cycle low-time filter would treat differently from the integer `gap >= 3` model? Mechanism (byte-identical to the default build): - `LockstepBus::run_ppu_to` already seeds the real M2-phase into the mapper `sub_dot` (pre-access/M2-low = 0, post-access/M2-high = 2) under the existing `mmc3-m2-phase-irq` lever. The new feature reuses that exact seeding via an `any(...)` cfg so the probe observes real per-dot phase on the promoted one-clock scheduler. - In `Mmc3::notify_a12_at_sub_dot` the original `if gap >= 3 && self.clock_irq()` is refactored into a nested form so the probe can observe a *qualifying* rise (gap accepted) independently of whether it went on to clock the counter. The refactor is behavior-identical: `clock_irq()` (which mutates) is still only evaluated when `gap >= 3`, exactly the short-circuit the original `&&` gave. - The probe ONLY increments four `u64` counters (qual_pre/qual_post, irq_pre/irq_post) -- it never touches `irq_pending_line` or any emulated state, so with the feature ON the timeline is byte-identical to the default. Tallies are surfaced via `MapperDebugInfo.extra` and read back by the fixture without adding trait methods. The entire probe compiles out when the feature is off (zero footprint on the shipped / `#![no_std]` build). Finding -- the "no post-access qualifying rise" premise is ROM-specific: - `mmc3_test_2/4` #3 and `mmc3_test_v1/4` #3 (scanline_timing): irq_post = 0, directly confirming Session B's (2026-07-02) indirect byte-identity result -- for these, axis B is dead. - `mmc3_test_v1/5` #2 and `/6` #2 (reload/set-IRQ-every-clock), which Session B never measured, produce 4 post-access IRQ-clocking rises each (every qualifying rise is post-access). So "no post-access rise" is a property of the scanline-timing ROMs' phase alignment, not a structural NTSC-MMC3 invariant. - Engaging the existing `mmc3-m2-phase-irq` rising-edge deferral on `/5`+`/6` leaves their failure status byte-identical -- that concrete lever is non-curative and stays on the DO-NOT-RETRY list. Disposition: the F5.0 by-design closure HOLDS for the shipping default and for every lever ever tested; its rationale shifts from "no post-access rise is structurally possible" to "no tested lever is curative." One genuinely-untested axis-B lever -- an ares-style M2-edge-precise falling-edge low-time filter, which needs a sacred-gate-risking scheduler-substrate change even to prototype -- is recorded in ADR 0002's 2026-07-11 decision update as a candidate deferred to a maintainer decision. The four residuals stay `#[ignore]`'d with their fail-loud `*_currently_fails` companions; AccuracyCoin stays 141/141 and the default build is byte-identical (no production, scheduler, or MMC3 default behavior changed). Docs: ADR 0002 gains the 2026-07-11 v2.1.5 F5.0 decision update (method, the per-ROM tally table, and the deferred-lever disposition); `docs/accuracy-ledger.md` is updated to the refined disposition; the CHANGELOG `[Unreleased]` records the probe + finding. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…pal_apu_tests) Close the final two PAL APU accuracy residuals — blargg `pal_apu_tests` `10.len_halt_timing` and `11.len_reload_timing` — by modelling the 2A03's one-step deferral of length-counter halt changes and length reloads relative to the frame sequencer's half-frame length clock. Extends PR #275 from 8/10 to 10/10 pal_apu_tests, with NTSC held byte-identical. Root cause ---------- The length counter applied a `$4000`-bit-5 halt change and a `$4003` length reload IMMEDIATELY, during the CPU's bus write. Because the CPU's bus access runs before the APU's per-cycle `tick_with_external` (which fires the half-frame length clock), a halt/reload write that landed on the exact cycle of a half-frame clock was applied BEFORE that clock — the inverse of silicon, which applies both AFTER. This surfaced only on PAL because the NTSC `blargg_apu_2005` 10/11 ROMs report via a `$6000` protocol they have no PRG-RAM for (a vacuous status==0), whereas the PAL suite decodes the on-screen verdict. PAL reported `FAILED #3` (len_halt: "should be clocked when halted at 16629" — the write-on-clock-cycle case) and `FAILED #4` (len_reload: "reload during clock when ctr = 0 should work"). Mechanism (mirrors TetaNES LengthCounter::reload + Mesen2 _newHaltValue) ----------------------------------------------------------------------- `LengthCounter` gains three deferral fields: * new_halt — latched by `set_halt` on a $4000/$4004/$4008/$400C write; promoted to the effective `halt` in `reload`. * reload_val — pending length load latched by `load` (deferred). * previous_count — snapshot of `count` at load time. `reload()` applies a pending reload only if the post-clock `count` still equals `previous_count` (a same-cycle half-frame clock did not decrement it because it was already zero), then refreshes `halt = new_halt`. The APU calls `reload()` on all four length channels once per CPU cycle in `tick_with_external`, AFTER `handle_frame_events` (the half-frame clock) and BEFORE the mixer samples channel outputs. NTSC byte-identity (sacred) --------------------------- On the common non-coincident write cycle the count is untouched between the write and `reload`, so `count == previous_count` holds and the reload settles in-cycle before the sample — identical to an immediate load; halt does not affect `output()` directly, so its deferral is invisible except at a clock. Only the write-lands-on-the-clock-cycle coincidence the ROMs probe differs. Verified byte-identical: AccuracyCoin 141/141, blargg_apu_2005 11/11, f2_accuracy_audit 6/6, apu_reset 4/4, apu_mixer / apu_test / volume_tests / nestest / visual_regression all unchanged against their pre-change goldens. The snapshot byte layout is unchanged: the deferral scratch fields are not serialized (they resolve within the write cycle), and `read_length` seeds `new_halt = halt` so a restore never spuriously clears a halted counter. pal_apu_tests 10 & 11 promoted from the fail-loud `pal_apu_residual!` pins to `pal_apu_pass!`; the now-unused macro is removed. Docs synced: docs/apu-2a03.md (new §Length halt/reload ordering), docs/accuracy-ledger.md (residual closed), docs/STATUS.md, docs/testing-strategy.md, CHANGELOG.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 pal_apu_tests Implement the PAL (2A07) APU frame-counter sequencer step positions, closing five of the seven PAL timing residuals the salvaged screen-reading oracle pinned. The blargg `pal_apu_tests` corpus now scores 8/10 under forced PAL region (was 3/10), with the two remaining failures documented as a bounded, fail-loud residual. Mechanism --------- The 2A03 (NTSC) and 2A07 (PAL) share the same six-step sequencer but divide the CPU clock differently, so the identical quarter/half/IRQ events land at different CPU-cycle counts. `FrameCounter` now carries a `pal: bool` selector, derived from the console `Region` by `Apu::new` (true only for `Region::Pal`; NTSC and Dendy — a PAL-clocked famiclone with an NTSC-period frame counter — keep the NTSC positions). The sequencer step tables are: - PAL 4-step (mode 0): 8313 / 16627 / 24939 / 33252 / 33253 / 33254 (quarter 8313/16627/24939/33253; half 16627/33253; IRQ 33252/33253/33254). - PAL 5-step (mode 1): 8313 / 16627 / 24939 / 41565 / 41566 (quarter 8313/16627/24939/41565; half 16627/41565; no IRQ). These are the canonical Mesen2 `stepCyclesPal` values. The mode-0 terminal three cycles replicate the NTSC IRQ-flag-visibility / `irq_line_active` split verbatim at the PAL positions, preserving the AccuracyCoin Frame-Counter-IRQ Tests I/J/K/L/M semantics for PAL. NTSC byte-identity (sacred constraint) -------------------------------------- The change is strictly region-gated: the `pal == false` branch runs the original NTSC step tables unchanged, and the power-on / snapshot-restore default is NTSC. The `tick` hot path was split into `clock_sequencer` + `four_step` / `five_step` helpers (to stay within the clippy line budget) with no behavioral change to the NTSC arms. Verified byte-identical: - AccuracyCoin 141/141 (100.00%) — unchanged. - `apu_test` 8/8, NTSC `blargg_apu_2005` 11/11, `nestest` 0-diff — unchanged. - APU snapshot format is untouched: `pal` is derived, not persisted; `Apu::restore` re-derives it from the restored region. Residual (honest) ----------------- `10.len_halt_timing` and `11.len_reload_timing` still fail, but the PAL step positions advanced them from `FAILED: #2` to `FAILED: #3` / `#4`. Their NTSC builds pass (`blargg_apu_2005` 10 & 11), localizing the gap to a PAL-specific length-counter halt/reload write-vs-half-frame-clock ordering detail adjacent to the frame-counter step model. Left as fail-loud residual pins and recorded in docs/accuracy-ledger.md; closing it is a separate deeper investigation. Tests + docs ------------ Adds seven PAL frame-counter unit tests (step positions, IRQ position, no-IRQ-at-NTSC-position, wrap, 5-step, NTSC-default). Promotes pal_apu 04-08 from residual pins to strict passes; keeps 10/11 as documented residuals. Updates docs/apu-2a03.md, docs/accuracy-ledger.md, docs/STATUS.md, docs/testing-strategy.md to the 8/10 state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pal_apu_tests) Close the final two PAL APU accuracy residuals — blargg `pal_apu_tests` `10.len_halt_timing` and `11.len_reload_timing` — by modelling the 2A03's one-step deferral of length-counter halt changes and length reloads relative to the frame sequencer's half-frame length clock. Extends PR #275 from 8/10 to 10/10 pal_apu_tests, with NTSC held byte-identical. Root cause ---------- The length counter applied a `$4000`-bit-5 halt change and a `$4003` length reload IMMEDIATELY, during the CPU's bus write. Because the CPU's bus access runs before the APU's per-cycle `tick_with_external` (which fires the half-frame length clock), a halt/reload write that landed on the exact cycle of a half-frame clock was applied BEFORE that clock — the inverse of silicon, which applies both AFTER. This surfaced only on PAL because the NTSC `blargg_apu_2005` 10/11 ROMs report via a `$6000` protocol they have no PRG-RAM for (a vacuous status==0), whereas the PAL suite decodes the on-screen verdict. PAL reported `FAILED #3` (len_halt: "should be clocked when halted at 16629" — the write-on-clock-cycle case) and `FAILED #4` (len_reload: "reload during clock when ctr = 0 should work"). Mechanism (mirrors TetaNES LengthCounter::reload + Mesen2 _newHaltValue) ----------------------------------------------------------------------- `LengthCounter` gains three deferral fields: * new_halt — latched by `set_halt` on a $4000/$4004/$4008/$400C write; promoted to the effective `halt` in `reload`. * reload_val — pending length load latched by `load` (deferred). * previous_count — snapshot of `count` at load time. `reload()` applies a pending reload only if the post-clock `count` still equals `previous_count` (a same-cycle half-frame clock did not decrement it because it was already zero), then refreshes `halt = new_halt`. The APU calls `reload()` on all four length channels once per CPU cycle in `tick_with_external`, AFTER `handle_frame_events` (the half-frame clock) and BEFORE the mixer samples channel outputs. NTSC byte-identity (sacred) --------------------------- On the common non-coincident write cycle the count is untouched between the write and `reload`, so `count == previous_count` holds and the reload settles in-cycle before the sample — identical to an immediate load; halt does not affect `output()` directly, so its deferral is invisible except at a clock. Only the write-lands-on-the-clock-cycle coincidence the ROMs probe differs. Verified byte-identical: AccuracyCoin 141/141, blargg_apu_2005 11/11, f2_accuracy_audit 6/6, apu_reset 4/4, apu_mixer / apu_test / volume_tests / nestest / visual_regression all unchanged against their pre-change goldens. The snapshot byte layout is unchanged: the deferral scratch fields are not serialized (they resolve within the write cycle), and `read_length` seeds `new_halt = halt` so a restore never spuriously clears a halted counter. pal_apu_tests 10 & 11 promoted from the fail-loud `pal_apu_residual!` pins to `pal_apu_pass!`; the now-unused macro is removed. Docs synced: docs/apu-2a03.md (new §Length halt/reload ordering), docs/accuracy-ledger.md (residual closed), docs/STATUS.md, docs/testing-strategy.md, CHANGELOG.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oracle (8/10, v2.1.5) (#275) * wip(pal-apu): salvage orphaned-agent PAL APU oracle work WIP snapshot of the v2.1.5 PAL APU oracle workstream, committed to protect the work after the spawning background agent was orphaned on a session exit. Not yet gate-verified; to be assessed and completed before PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(apu): model PAL frame-counter step positions (region-gated); 8/10 pal_apu_tests Implement the PAL (2A07) APU frame-counter sequencer step positions, closing five of the seven PAL timing residuals the salvaged screen-reading oracle pinned. The blargg `pal_apu_tests` corpus now scores 8/10 under forced PAL region (was 3/10), with the two remaining failures documented as a bounded, fail-loud residual. Mechanism --------- The 2A03 (NTSC) and 2A07 (PAL) share the same six-step sequencer but divide the CPU clock differently, so the identical quarter/half/IRQ events land at different CPU-cycle counts. `FrameCounter` now carries a `pal: bool` selector, derived from the console `Region` by `Apu::new` (true only for `Region::Pal`; NTSC and Dendy — a PAL-clocked famiclone with an NTSC-period frame counter — keep the NTSC positions). The sequencer step tables are: - PAL 4-step (mode 0): 8313 / 16627 / 24939 / 33252 / 33253 / 33254 (quarter 8313/16627/24939/33253; half 16627/33253; IRQ 33252/33253/33254). - PAL 5-step (mode 1): 8313 / 16627 / 24939 / 41565 / 41566 (quarter 8313/16627/24939/41565; half 16627/41565; no IRQ). These are the canonical Mesen2 `stepCyclesPal` values. The mode-0 terminal three cycles replicate the NTSC IRQ-flag-visibility / `irq_line_active` split verbatim at the PAL positions, preserving the AccuracyCoin Frame-Counter-IRQ Tests I/J/K/L/M semantics for PAL. NTSC byte-identity (sacred constraint) -------------------------------------- The change is strictly region-gated: the `pal == false` branch runs the original NTSC step tables unchanged, and the power-on / snapshot-restore default is NTSC. The `tick` hot path was split into `clock_sequencer` + `four_step` / `five_step` helpers (to stay within the clippy line budget) with no behavioral change to the NTSC arms. Verified byte-identical: - AccuracyCoin 141/141 (100.00%) — unchanged. - `apu_test` 8/8, NTSC `blargg_apu_2005` 11/11, `nestest` 0-diff — unchanged. - APU snapshot format is untouched: `pal` is derived, not persisted; `Apu::restore` re-derives it from the restored region. Residual (honest) ----------------- `10.len_halt_timing` and `11.len_reload_timing` still fail, but the PAL step positions advanced them from `FAILED: #2` to `FAILED: #3` / `#4`. Their NTSC builds pass (`blargg_apu_2005` 10 & 11), localizing the gap to a PAL-specific length-counter halt/reload write-vs-half-frame-clock ordering detail adjacent to the frame-counter step model. Left as fail-loud residual pins and recorded in docs/accuracy-ledger.md; closing it is a separate deeper investigation. Tests + docs ------------ Adds seven PAL frame-counter unit tests (step positions, IRQ position, no-IRQ-at-NTSC-position, wrap, 5-step, NTSC-default). Promotes pal_apu 04-08 from residual pins to strict passes; keeps 10/11 as documented residuals. Updates docs/apu-2a03.md, docs/accuracy-ledger.md, docs/STATUS.md, docs/testing-strategy.md to the 8/10 state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): rewrite the v2.1.5 PAL APU entry to the 8/10 implemented state The salvaged WIP entry described the pre-implementation state (screen-reading oracle wired, 3/10 honest pass, PAL frame counter unmodeled). Rewrite it to record the delivered work: the region-gated PAL (2A07) frame-counter step positions, the resulting 8/10 pass count (04-08 flipped to PASS), the preserved NTSC byte-identity (AccuracyCoin 141/141, apu_test 8/8, blargg_apu_2005 11/11, nestest 0-diff), and the two documented length halt/reload timing residuals (10/11) pinned fail-loud. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(apu): defer length halt/reload ordering; close PAL 10/11 (10/10 pal_apu_tests) Close the final two PAL APU accuracy residuals — blargg `pal_apu_tests` `10.len_halt_timing` and `11.len_reload_timing` — by modelling the 2A03's one-step deferral of length-counter halt changes and length reloads relative to the frame sequencer's half-frame length clock. Extends PR #275 from 8/10 to 10/10 pal_apu_tests, with NTSC held byte-identical. Root cause ---------- The length counter applied a `$4000`-bit-5 halt change and a `$4003` length reload IMMEDIATELY, during the CPU's bus write. Because the CPU's bus access runs before the APU's per-cycle `tick_with_external` (which fires the half-frame length clock), a halt/reload write that landed on the exact cycle of a half-frame clock was applied BEFORE that clock — the inverse of silicon, which applies both AFTER. This surfaced only on PAL because the NTSC `blargg_apu_2005` 10/11 ROMs report via a `$6000` protocol they have no PRG-RAM for (a vacuous status==0), whereas the PAL suite decodes the on-screen verdict. PAL reported `FAILED #3` (len_halt: "should be clocked when halted at 16629" — the write-on-clock-cycle case) and `FAILED #4` (len_reload: "reload during clock when ctr = 0 should work"). Mechanism (mirrors TetaNES LengthCounter::reload + Mesen2 _newHaltValue) ----------------------------------------------------------------------- `LengthCounter` gains three deferral fields: * new_halt — latched by `set_halt` on a $4000/$4004/$4008/$400C write; promoted to the effective `halt` in `reload`. * reload_val — pending length load latched by `load` (deferred). * previous_count — snapshot of `count` at load time. `reload()` applies a pending reload only if the post-clock `count` still equals `previous_count` (a same-cycle half-frame clock did not decrement it because it was already zero), then refreshes `halt = new_halt`. The APU calls `reload()` on all four length channels once per CPU cycle in `tick_with_external`, AFTER `handle_frame_events` (the half-frame clock) and BEFORE the mixer samples channel outputs. NTSC byte-identity (sacred) --------------------------- On the common non-coincident write cycle the count is untouched between the write and `reload`, so `count == previous_count` holds and the reload settles in-cycle before the sample — identical to an immediate load; halt does not affect `output()` directly, so its deferral is invisible except at a clock. Only the write-lands-on-the-clock-cycle coincidence the ROMs probe differs. Verified byte-identical: AccuracyCoin 141/141, blargg_apu_2005 11/11, f2_accuracy_audit 6/6, apu_reset 4/4, apu_mixer / apu_test / volume_tests / nestest / visual_regression all unchanged against their pre-change goldens. The snapshot byte layout is unchanged: the deferral scratch fields are not serialized (they resolve within the write cycle), and `read_length` seeds `new_halt = halt` so a restore never spuriously clears a halted counter. pal_apu_tests 10 & 11 promoted from the fail-loud `pal_apu_residual!` pins to `pal_apu_pass!`; the now-unused macro is removed. Docs synced: docs/apu-2a03.md (new §Length halt/reload ordering), docs/accuracy-ledger.md (residual closed), docs/STATUS.md, docs/testing-strategy.md, CHANGELOG.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(test-harness): neutral pal_apu_pass message + reuse screen decode buffer Address two PR #275 bot-review threads (both behavior-preserving): - **Copilot (pal_apu_tests.rs):** the `pal_apu_pass!` macro's failure message called every ROM a "region-independent APU check", but the macro also covers the PAL frame-counter-timing checks (04-08) and the length halt/reload checks (10-11), for which that claim is false. Reworded to a neutral "PAL APU {name}: expected on-screen PASSED but ROM reported {verdict}" and updated the macro doc to enumerate all three categories it now covers. - **Gemini (nes_runner.rs `decode_screen_text`):** the decoder allocated a fresh `String` per frame and a per-row intermediate `String` (~60 allocs per frame, ~100k+ per multi-hundred-frame run). Refactored to `decode_screen_text_into(nes, &mut String)`: `run_nes_screen` now decodes every frame into ONE reused buffer (`clear`ed each frame, moved into the result at the end). Trailing-space trimming and blank-row dropping are done in place by tracking each row's start offset + last non-space byte and `truncate`-ing back to it (every glyph is ASCII `0x20..=0x7e`, so one byte per char and the offsets are always on a char boundary) — no per-row scratch String. Decoded text is byte-for-byte identical; verified pal_apu_tests still 10/10 with unchanged on-screen verdicts and frame counts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…se channels and the frame counter (#455) * feat(cosim): export the APU's integer channel levels for rung 4 Rung 4's gate needs what each APU channel hands its DAC, per CPU cycle. The oracle already carried the other two surfaces -- $4015 reads through the existing bus trace, and the APU's /IRQ at both M2 phases in irq.csv -- so this adds the one that was missing. --apu-trace CAP writes <stem>.apu.bin: 16-byte little-endian records, a u64 CPU cycle then pulse 1, pulse 2, triangle, noise and DMC as u8, then three pad bytes. The cycle is stored EXPLICITLY rather than implied by the index, because the records are drained per frame and an index-implied cycle would be wrong the moment a frame boundary shifted anything. The five INTEGER levels only. MixRecord's `mixed` and `external` are f32 and are DIAGNOSTICS per RustyNES_MiSTer/docs/rung4-apu.md -- they are RustyNES's model of an analog stage (the non-linear lookup, the band-limited decimator, expansion gain), and exporting them would invite a gate against an emulator's arithmetic rather than against the chip. They are deliberately not written. Oracle::advance_frames drains the APU's store at every frame boundary, because MixTrace is sized for ONE frame and Nes::run_frame re-anchors it at its own start. Skipping the drain would lose every frame but the last, and would do it silently -- the resulting golden is a well-formed shorter file. Nothing re-anchors from this side: run_frame owns that, and a second clear could only ever be wrong since it would run at a different cycle. rustynes-cosim gains the `debug-hooks` feature, which reaches rustynes-apu/debug-hooks. Safe because this crate is excluded from the workspace -- the exclusion is what stops a trace feature unifying into the accuracy battery's build of the core, and cosim_manifest_audit asserts it still holds. The feature is compile-time and the runtime arm is separate and default-off, so it changes no emulated behaviour: VERIFIED, not assumed -- exporting a golden with and without it produces byte-identical obs.bin, index_fb.bin and ram.bin. Capacity is validated at the boundary and a dropped record FAILS rather than warning, matching --fetch-trace: a truncated golden is not a smaller golden, because the comparator sees a length mismatch and reports a divergence whose real cause is an export-side capacity. Both capacity validators are extracted into named functions, which keeps parse_args under the line limit and puts each one's reasoning next to the check rather than in the middle of an arg loop. Device-under-test work is in RustyNES_MiSTer@d6bef7d. * chore(release): cut v2.5.9 "Overture" -- rung 4 opens with the two pulse channels and the frame counter An overture is the part that plays before the rest of the orchestra arrives, which is what two of five channels is. rtl/apu2a03.sv in the sibling (RustyNES_MiSTer@d6bef7d) lands both pulse channels -- timer, 8-step duty sequencer, length counter, envelope and the sweep MUTE -- plus the frame counter in both modes with its IRQ and the $4015/$4017 register file, written from the NESdev wiki with no emulator or HDL source consulted per ADR 0037. Triangle, noise and DMC are v2.6.0 and v2.6.1. The partition was fixed BEFORE the rung, because the APU is the hardest chip in the console to gate honestly: what it produces is an analog level and what an emulator computes is a number. Gates are the $4015 read value, the /IRQ pin and each channel's INTEGER DAC input, all per CPU cycle. Diagnostics are MixRecord's f32 mixed and external fields -- RustyNES's non-linear mixer, band-limited decimator and expansion gain -- the frame-sequencer step index, and apu_phase. A field that exists only because RustyNES chose to model something that way never becomes a gate, however convenient. The stimulus measurement earned its place immediately, finding four ROM defects before a single gate ran: length index 3 is 2 and not 254, because the table alternates long and short and the index is not the count; the 6502 boots with I set, so without CLI zero IRQs are taken despite five real line edges, and the interrupt SEQUENCE is half of what that ROM gates; two channels at the same volume are indistinguishable in a channel-level golden; and power-on work RAM is SEEDED rather than zeroed, so an uninitialised counter byte came up 0x7D and the handler's CMP #3 never matched. Four findings in the DUT. The duty sequencer counts UP -- counting down gave the right period and the right levels with the wrong phase, pulse 1 three steps late and pulse 2 five. The 4-step sequence constants must be consistently 0-based, since fc_count reads V on tick V+1: three of four were and the last was written as the wiki's own number, putting the frame IRQ 3 CPU cycles late (29,830 against 29,827, measured) and the last length clock of every frame with it. $4017 bit 7 clocks a quarter and half frame IMMEDIATELY, where a latched flag clocked on the next APU tick left exactly two divergent cycles at a length expiry. And the $4017 sequencer-reset delay depends on bit 7, which the wiki's "3 or 4 CPU clock cycles" does not settle -- each constant fixed one ROM and broke the other, and a parity rule on apu_phase separated nothing because both ROMs' writes land on the same phase. Gates: apulen027 exact on both surfaces at 178,668 cycles each; apupulse026's bus surface 3 of 178,668 and its channel levels 1,000 of 178,668 -- which is 500 runs of EXACTLY TWO cycles, one per pulse edge, a uniform one-tick offset rather than a structural fault. It is a phase sensitivity the first stimulus hid: adding a five-cycle counter initialisation, an ODD number, flipped which apu_phase the $4003 writes land on, and before that change both ROMs were exact. Two candidate fixes were tried and both rejected by measurement -- flipping the pulse timer's tick phase halved one ROM while breaking the other, and reloading the period divider on $4003 against the wiki's explicit "the period divider is not reset" made both worse. Carried to v2.6.0 with the ROM that exposes it already written. Nine of ten mutations CAUGHT, and the two that were not both indicted the STIMULUS rather than the gate: a halt flag whose channel's length was too long to expire inside the window, and an inhibit bit only ever set in 5-step mode where the IRQ cannot fire anyway. Both ROMs were changed and both are now caught. The tenth is the sweep mute's threshold, out of stimulus regardless -- both ROMs use periods 64 and 84, so any threshold below 64 is inert -- and the mute belongs to the sweep unit this rung defers. Oracle side: --apu-trace on nes_golden_export exports the five integer channel levels per CPU cycle with an EXPLICIT cycle per record, because the records are drained per frame and an index-implied cycle would be wrong the moment a frame boundary shifted anything. rustynes-cosim enables debug-hooks, safe because the crate is excluded from the workspace and cosim_manifest_audit asserts that still holds; the feature is compile-time and the runtime arm is separate, so it changes no emulated behaviour -- VERIFIED, not assumed, with byte-identical obs.bin, index_fb.bin and ram.bin. The release-line chain gate added during the v2.5.8 review caught its first real drift on this cut -- and then failed on its own parser, which is worth recording: it walked back to the nearest `v` before ", the current release", and "Overture" contains one. It now walks back to the nearest `v` that actually BEGINS a version. Re-mutated: stale version, phrase removed entirely (fail-closed), and version removed while the phrase remains -- all three caught. Zero emulation-core changes, so AccuracyCoin holds 141/141 (100.00%, RAM decoder) and nestest stays 0-diff by construction. Gates: cargo fmt clean; clippy --workspace --all-targets -D warnings clean; release_anchor_audit 9/9, release_state_prose_audit 8/8, release_notes_render_audit 2/2, libretro_info_audit 3/3, cosim_manifest_audit 4/4; markdownlint passed on every changed file. Per the amended upstream cadence this release opens NO libretro/RetroArch sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj * fix(cosim,audit): address the CodeRabbit review on the v2.5.9 cut Four findings, all four verified against the tree before any fix. TWO MAJOR, both real and both the same class -- a check that reports a pass it has not earned. --apu-trace combined with --inject-instructions wrote a well-formed EMPTY .apu.bin with a dropped count of zero. The trace drains at frame boundaries, in advance_frames, and an injection run steps instructions and completes no frames, so drain_apu_frame was never reached. An empty golden whose emptiness is indistinguishable from a run that produced nothing is exactly what this project keeps catching, so the combination is now REFUSED in injection_error rather than warned about -- a warning on stderr is a signal that may never arrive, and this project has already lost one that fired on every run. The release-line chain gate's reverse `v` scan accepted a `v` inside a word: `rev2.5.9, the current release` parsed as version 2.5.9 and satisfied the audit while containing no version token at all. The `v` must START a token. Both now have regression tests that call the REAL validation path, and both are demonstrated to fail. The second one needed the fix this project recorded in v2.4.0: the scan was inline in the audit, so the test restated it as a local closure and would have agreed with itself forever. It is extracted as `version_before` and both the audit and the test call it -- mutating the production function now fails the test, which mutating a copy never could. Worth noting how the two mutation runs went, because both first attempts were worthless in the way this project keeps recording. The --apu-trace mutation reported "test result: ok" from the LIB target while the bin target's result -- the one with the test in it -- was the second line; `head -1` took the empty one. And the scan mutation's pattern did not apply at all, so the run measured unmutated source. Neither was a catch and both were re-run with the test count checked first. TWO MINOR, both real. ROADMAP.md's Last Updated said 2026-08-23 while the same file recorded v2.5.9 as released on the 24th. And to-dos/mister/SPRINT_PLAN.md's results pointer listed rung 1 and rung 3 but not the rung-4 document this release adds -- the sprint plan named a completed rung without pointing at its evidence. Gates: release_anchor_audit 10/10 (the new test included), release_state_prose_audit 8/8, release_notes_render_audit 2/2, libretro_info_audit 3/3, cosim_manifest_audit 4/4; rustynes-cosim's own suite green; cargo fmt clean; clippy -D warnings clean on both the workspace and the excluded cosim crate; markdownlint passed. * fix(cosim): backstop an armed-but-empty APU golden, and name the record padding Both raised by the Antigravity reviewer, which found the --apu-trace/injection data loss independently of CodeRabbit -- two reviewers converging on the same defect is worth noting, since it was a silent one. The upstream guard in injection_error refuses the one combination known to produce an empty golden. This adds the invariant underneath it: the trace being ARMED and producing no records is a defect, not a short run, and any future path that arms without draining would otherwise ship a 0-byte golden and report success because `dropped` stays 0. The two fail for different reasons and that is the point -- one states the rule, the other states the invariant. Also names the record padding: 13 data bytes (8 cycle + 5 levels) out to APU_REC_LEN, chosen as a power of two so the comparator's indexing stays trivial and so v2.6.0/v2.6.1's three channels arrive without changing the record size under an existing golden. * style(cosim): apply rustfmt to the injection-refusal assertion The `assert!` added for `apu_trace_with_injection_is_refused` exceeded the line budget with its message argument inline, so rustfmt wanted it split across four lines. It was written by hand and never passed through the formatter, which is easy to do in this crate specifically: `rustynes-cosim` is EXCLUDED from the workspace, so `cargo fmt --all` from the repository root does not reach it. CI has a separate "rustfmt (excluded crate: rustynes-cosim)" step for exactly that reason, and that step is what caught this. No behaviour change -- the assertion, its condition and its message are byte-identical, only the line breaks move. * fix(cosim): bound --apu-trace capacity and reject a zero-frame run Four findings from the Antigravity review of the v2.5.9 branch, one of them blocking. Each was verified against the source before being fixed rather than taken on the reviewer's word. UNBOUNDED ALLOCATION (blocking, confirmed) parse_apu_cap checked only for zero, while its sibling parse_fetch_cap has bounded against fetch_trace::MAX_CAPACITY since it was written. The capacity reaches Vec::with_capacity(capacity * APU_REC_LEN) inside Oracle::enable_apu_trace, so `--apu-trace 99999999999999` asks the allocator for more memory than exists and aborts the process. The asymmetry between two adjacent parsers is what made it easy to miss. MAX_APU_TRACE_CAPACITY is 1 << 23 records -- one record is one CPU cycle, so 8,388,608 cycles, roughly 281 NTSC frames, 134 MB once multiplied. Comfortably above any legitimate rung-4 stimulus (a 24-frame run wants ~715,000) and bounded. The CLI REFUSES an over-large value rather than clamping it, because a silently clamped capacity produces a short golden that claims to cover the whole run -- the same failure mode the dropped-record check exists to prevent. enable_atrace clamps as well, but only as a library-level backstop for callers that bypass the CLI, and `cap` takes the CLAMPED value so the dropped accounting is measured against what the buffer actually holds. Both halves are pinned by tests and both were demonstrated to fail by mutation: removing the clamp reproduces the allocator abort in the test itself, and storing the unclamped cap alongside a clamped buffer fails the accounting assertion. A ZERO-FRAME RUN PANICKED INSTEAD OF REPORTING BAD INPUT `--frames 0` never reaches a frame boundary, so an armed APU trace is never drained, and write_apu_trace's emptiness invariant then fires with a message blaming "some run path" for what is plainly invalid input. --frames is now rejected at the boundary. The invariant stays: it is the backstop for paths nobody has thought of, and it should not be the first line of defence against an argument. A TRUNCATED GOLDEN WAS WRITTEN BEFORE THE FAILING EXIT write_apu_trace wrote the file and then exited non-zero on a dropped record. The exit code fails a pipeline; the file outlives it, and a truncated .bin looks like every other golden on disk, so the next gate run compares a window shorter than the manifest claims. The dropped check now runs BEFORE the write and says "No file written." write_fetch_trace has the same shape and is deliberately left alone -- it is pre-existing and unrelated to this release's subject, and widening a release PR to touch it is how a scoped change stops being one. DIVISION IN A PER-RECORD LOOP drain_apu_frame tested `acc.bytes.len() / APU_REC_LEN >= acc.cap` on every iteration; it now multiplies the constant instead. Behaviour-identical for every reachable value, and saturating so the bound cannot wrap. * fix(cosim): rejoin a doc comment the new tests were inserted into The two `--apu-trace` capacity tests were inserted at the wrong offset and landed INSIDE the doc comment of `five_frames_requested_is_five_frames_of_ cycles`, truncating it mid-sentence at "A bare" and orphaning its last line above the following test. clippy's `empty_line_after_doc_comments` caught it, correctly, as a doc comment separated from the item it documents. The tests move to after that function; the comment is rejoined verbatim. WHY IT REACHED CI. I verified locally with `cargo clippy --all-targets -- -D warnings 2>&1 | grep -E "^(error|warning)"` and read the empty output as clean. clippy emits ANSI colour codes at the start of every diagnostic line, so the anchored pattern could not match -- the filter was structurally incapable of reporting the failure it was written to catch. That is the project's own recurring trap: a pattern that cannot match looks exactly like content that is not there. The verification here reads the EXIT CODE instead, which is the only signal that cannot be filtered away. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ecome checkable (#494) * docs(plan): v2.6.15 "Warrant" — the plan, and a diagnosis it already corrects v2.7.0 is the submission email. The contributing page states the bar for AI-assisted code as "a minimum reasonable bar for readability and include some evidence of quality and accuracy testing", and this project has that evidence -- 142 co-simulation gates with a mutation record each -- in a form a reviewer cannot run, because tb/regress.sh says in its own header that it is not a CI gate and cannot be. So v2.6.15 is the release that makes those claims checkable, and it opens on two research findings that change its shape. The first is that the .rbf name this project has shipped since v2.6.7 would distribute NOTHING. Distribution_MiSTer's builder requires a stem whose last nine characters are an underscore plus exactly eight digits and skips any file that does not match; Main_MiSTer's firmware applies a different rule again. That was carried as a style divergence awaiting a decision. It is a hard blocker, and the failure mode is an accepted core that ships nothing with no error raised anywhere. The second is that "needs no SDRAM" is a differentiator. The MiSTer wiki's Cores-that-use-SDRAM table -- which is the live registration database the distribution parses, not documentation -- reads Yes for the NES, and the FAQ gives the NES as its own worked example of why. This core's on-die build does not have that problem, which makes the on-die build the submission artifact rather than a fallback. The plan also records a diagnosis this session has already had to correct. T-ORACLE-001 claims RustyNES never clocks the MMC3 counter on the pre-render line. The corpus refutes it: mmc3_test_2/2-details sub-test 8 is literally "Counter should be clocked 241 times in PPU frame", and RustyNES passes it. The claim came from a PPU state trace, which the ticket's own instrument-traps section says carries no CHR address column and therefore cannot see an A12 rise at all. The plan keeps the workstream and states the refutation condition up front. Maintainer decisions recorded in the plan: ship both .rbf names, hold v2.7.0 until a board exists, and prepare groundwork for all four v2.8.0 tracks. * fix(mmc3): two of the four R1/R2 residuals were never IRQ-timing residuals ADR 0002 has closed this residual over four sub-tests since v2.0.0 beta.3 -- mmc3_test_2/4 #3, mmc3_test_v1/4 #3, mmc3_test_v1/5 #2 and mmc3_test_v1/6 #2 -- through 21+ documented rollbacks, a two-session bounded-effort campaign, and two instrumentation studies. Two of those four do not depend on when the IRQ asserts, so no lever on any axis the ADR searched could ever have moved them. They were mis-filed once and inherited unexamined through every update since. The corpus says so, and it says so twice. mmc3_test and mmc3_test_2 are the same suite twice over, and the second is the revision: mmc3_test_2 ships an "MMC3 Operation" readme section the older corpus lacks entirely -- kevtris's document corrected in three places, the revision A / B split, and the pathological $C001 sequence the author recommends nobody implement. And sub-test 2 of 5-MMC3 carries the IDENTICAL set_test string in both while differing by one instruction: the successor inserts a SECOND clock_counter before the first should_be_set, so its verdict no longer lands on the $C001-pending reload. The assertion was withdrawn by its author. mmc3_test_v1/6-MMC6 rests on the same withdrawn clock, and is additionally the v1 corpus's ALTERNATE-revision ROM -- its header names Crystalis, which mmc3_test_2's readme identifies as revision A, and mmc3_test_2/6-MMC3_alt carries that header verbatim. Measured rather than argued. Making clock_irq's path 1 assert on any $C001 reload landing on zero -- the rule the withdrawn assertion demands -- makes mmc3_test_v1/5 PASS and moves /6 from #2 to #3, and costs BOTH scanline_timing ROMs a regression from sub-test 3 to sub-test 2. Sub-test 2 is "should occur later", so the IRQ starts arriving too early on the one ROM pair that measures when it arrives. Reverted from a pre-edit snapshot; the numbers are in the ADR, because a rejected change with its measurement is a result. The R1/R2 residual is therefore mmc3_test_2/4 #3 and mmc3_test_v1/4 #3 -- one behaviour measured twice -- and the closure over those two stands untouched. This commit also RETRACTS the opening claim of T-ORACLE-001, in place. It says RustyNES never clocks the MMC3 counter on the pre-render line. It does: mmc3_test_2/2-details sub-test 8 is, verbatim, "Counter should be clocked 241 times in PPU frame" -- 240 visible plus pre-render -- and RustyNES passes it, as it has for every release this suite has run. The claim came from --ppu-state-trace, which that ticket's own instrument-traps section says carries no CHR address column and therefore cannot see an A12 rise at all. A trace that cannot see the event was read as evidence the event did not happen. The ticket is corrected in place rather than deleted, because the v2.6.15 plan cites it. Behaviour is unchanged: the mmc3 verdicts are byte-identical to the baseline captured before any edit, and the default gate is 18 passed / 5 ignored. The emulation core is untouched, so AccuracyCoin 141/141 and nestest 0-diff hold by construction. * fix(release): the bump script inserted a release at the wrong end of the chain bump_release.py has dropped the previous release from ROADMAP.md's lineage in two consecutive releases, v2.6.13 and v2.6.14. Both times release_anchor_audit caught it, both times it was repaired by hand, and v2.6.14 recorded it as a script defect and deferred the fix on the stated grounds that the gate makes deferring safe. It does. It also means the operator learns about it from a red audit AFTER committing, which is the part that stopped being acceptable when it happened twice. Reproduced in a throwaway worktree rather than reasoned about, and the CHANGELOG's description turns out to be wrong in a way that matters. The release does not vanish: it is RELOCATED. The CHAIN handler inserted the outgoing release after the first ", on ", which is the correct anchor for the shape the rule was written for (`..., on **v2.4.3 "Touchstone"** ...`) and the WRONG one for root ROADMAP.md, whose chain is thirty entries long and ends ", on the v2.0.0 'Timebase' MAJOR cut." So the token swap took v2.6.14 off the head and the insertion filed it beside the MAJOR cut, thirty entries away: before: v2.6.14 "Docket" released - ... Built on **v2.6.13 "Slack"** and ... after: v2.6.15 "Warrant" released - ... Built on **v2.6.13 "Slack"** and ... ... and v2.4.1 "Fabric", on **v2.6.14 "Docket"** and the v2.0.0 cut. A lineage that skips a release AND names it somewhere nonsensical is worse than one that merely skips it, because the release IS present and a reader checking for its absence finds it. The fix tries "Built on " first, because that names the HEAD of the chain, and falls back to ", on " only when there is no "Built on " to find. Neither present is reported, never bumped mechanically. The second shape is NOT mechanically fixable and now says so. A chain ending "..., the current release" is a claim about which release is current, and extending it needs a written summary of the release, which no script has. The script previously said nothing at all about these and exited 0. It now scans the six documents release_anchor_audit's chain-tail gate reads, names every chain still owed prose, and exits 1 -- refusing, rather than forgetting. The CHAIN rewrite is EXTRACTED from main() into extend_chain(), and that is the load-bearing part of this commit rather than tidying. The selftest named "chain names the predecessor once" re-implemented the substitution inline with its own re.sub, so the one code path that had actually gone wrong twice was the one nothing executed. A test that reimplements its subject agrees with itself forever. Nine selftests now call the shipped function, including root ROADMAP.md's real shape -- a "Built on" chain that ALSO contains a ", on " near its tail, which is the case that went wrong. Demonstrated by mutation: reverting extend_chain to the pre-v2.6.15 rule makes "chain: inserts at the HEAD, not the tail" and "chain: the tail is left alone" FAIL and the selftest exit 1, while every other check still passes; restored, exit 0. Verified end to end in a worktree with a stub CHANGELOG section: the chain head becomes "Built on **v2.6.14 'Docket'** and **v2.6.13 'Slack'**", the v2.0.0 tail is untouched, and release_anchor_audit goes from three failures to two. Both remaining are documented manual steps -- the VERSION-PLAN row and the chain summary -- and the script now announces the second one instead of leaving it to be discovered. * docs(status): correct three expired claims the v2.6.15 sweep re-measured docs/STATUS.md's lineage carried "sys/ is still empty; there is no .rbf" in the present tense inside a historical entry. The tense is corrected and when it stopped being true is named -- v2.6.6 vendored the framework, v2.6.7 shipped the first bitstream -- rather than the sentence deleted, because what a release did not yet have is part of what it was. to-dos/mister/IMPLEMENTATION_PLAN.md's "Where the core actually is" table is present tense and was eight releases out of date, claiming the APU, the cartridge, the SDRAM controller and sys/ were all "Not started" and the .rbf "Never produced" -- every one of which had shipped. Five rows corrected against the current state, and the table now carries a comment saying why a present-tense table goes stale silently and what to do about it. to-dos/mister/SPRINT_PLAN.md's SDRAM row said "Deferred; needs hardware", which v2.6.13 refuted by writing the controller, a four-way arbiter and a console bridge from the AS4C32M16SB-7 datasheet and accepting them against a behavioural part model. The blocker applied to ACCEPTANCE against the real part, not to construction -- rung 7's own recorded lesson, repeating one row below where it is written. Four more milestone rows re-measured, including M20, which is now BLOCKED on hardware by the maintainer decision taken while planning v2.6.15. * docs(checklist): tick the naming box, and revisit the .srf before a reviewer asks The naming box is TICKED. v2.6.15 resolved it -- releases/ carries RustyNES_YYYYMMDD.rbf, the GitHub releases carry that plus the version-named copy -- and the entry now records that the cost was worse than it said. It read "has no effect until submission". It has no effect until submission and then it has a total one: Distribution_MiSTer's builder skips a file that yields no datecode, so an accepted core would appear in the Cores table and ship nothing, with no error anywhere. The .srf box is REVISITED rather than ticked, and the revisit corrects a clause of its own recorded reason. That reason said the PLL warning "carries no message ID at all, so there is nothing for an assignment to name". True of MESSAGE_DISABLE, and not true of the mechanism .srf uses: Template_MiSTer ships Template.srf, and it suppresses that very warning with a rule keyed on ID 9999 and the literal text RST, plus four more matching the full sentence. There is something to name; it is simply not a MESSAGE_DISABLE ID. Two upstream sources disagree about whether the file is required at all -- the contributing wiki lists it among the standard files "required by the template", the Template's own Readme calls it "optional file to disable some warnings which are safe to disable" -- and the template ships two of them, so "required by the template" is at least a file the template has. It stays absent, and the reason changes from an impossibility to a COST: an .srf changes the compile's warning set, which tb/check_warnings.py pins, so adopting one means a full Quartus run and a regenerated baseline. The instance paths need adapting too, since Template.srf names emu:emu|pll:pll|pll_0002:pll_inst and this core instantiates its PLL differently -- the same naming difference that cost v2.6.6 -13.901 ns when sys_top.sdc's clock-group glob matched nothing. Scheduled for the next release that rebuilds the bitstream. Checklist gate green at 14 tests, and the list still has unticked boxes, which that gate deliberately requires. * docs(roadmap): measure whether a gate can reach hps_io, instead of asserting it T-MISTER-SAVE is the only deferred MiSTer item with a user-visible cost: cart.sv has 8 KiB of PRG-RAM and every battery MMC1 or MMC3 game -- Zelda, Final Fantasy, Kirby's Adventure, Crystalis -- loses its saves at power-off. v2.6.12 attempted it and refuted its own attempt on the grounds that "every save route terminates in hps_io, which no gate here instantiates". That is a claim about whether it CAN be instantiated, and nothing had tested it. Running Verilator against the vendored module answers it: without a CONF_STR value it stops at one error, because that parameter has no default; with one supplied it reaches elaboration and produces eleven, nearly all PROCASSWIRE from a single line assigning four wires procedurally -- which Verilator treats as an error and Quartus does not. So it is close, and not free, and that makes the design choice a real one rather than a guess. Instantiating the real module needs -Wno-PROCASSWIRE and siblings, and suppressing errors in vendored framework code to build a testbench is a decision rather than a flag: it weakens the lint gate for everything in that invocation, and sys/ is the one tree this project may not fix at source. Modelling the protocol instead is narrower and needs no suppression, and tests the CORE's side of the contract, which is the side that can be wrong -- against which a model of an interface can agree with the core about a protocol they both misread, the "agreement about an unasked question" failure this project has hit before. Recommended: the model, with the real module as a later cross-check -- the same shape as tb/sdram_model.sv, a behavioural part written from a datasheet whose own documentation states what it cannot see. One enabler serves three tickets: this one, T-MISTER-SAVESTATE, and T-MISTER-CHEATS through ioctl index routing. No code here. The measurement is the deliverable, and it replaces a blocker nobody had tested with a cost somebody can weigh. * chore(release): cut v2.6.15 "Warrant" — the claims v2.7.0 will make become checkable v2.7.0 is the submission email, and the contributing page states the bar for AI-assisted code in one sentence: "Fully AI generated code should meet a minimum reasonable bar for readability and include some evidence of quality and accuracy testing." This project has that evidence -- 142 co-simulation gates with a mutation record apiece -- and tb/regress.sh says in its own header that it is not a CI gate and cannot be. So the strongest thing here was a set of documents describing checks a reader cannot run. This release is about closing that gap, and it opened on a finding carried for eight releases as a matter of style. THE .rbf NAME THIS CORE SHIPPED WOULD HAVE DISTRIBUTED NOTHING. Distribution_MiSTer strips a datecode by taking the stem's last nine characters, requires an underscore plus exactly eight digits, and skips outright any file that yields none. A version-named bitstream is invisible to it: an accepted core would appear in the wiki Cores table and ship nothing, with no error anywhere. A second parser in the firmware has a different rule, and _YYYYMMDD is the only form satisfying both -- so two names, because there are two audiences and only one is a parser. TWO OF THE FOUR R1/R2 RESIDUALS WERE NEVER IRQ-TIMING RESIDUALS. ADR 0002 has closed that set since v2.0.0 through 21+ rollbacks and two instrumentation studies, and two of the four do not depend on when the IRQ asserts at all. mmc3_test and mmc3_test_2 are the same suite twice; sub-test 2 of 5-MMC3 carries the identical set_test string in both and differs by one instruction, the successor declining to assert on the clock this emulator fails. blargg withdrew it. Adopting it anyway was measured: one ROM passes, both scanline_timing ROMs regress from sub-test 3 to 2. Reverted, with the numbers. T-ORACLE-001's OPENING CLAIM IS RETRACTED. It says RustyNES never clocks the MMC3 counter on the pre-render line. It does -- mmc3_test_2/2-details sub-test 8 is verbatim "Counter should be clocked 241 times in PPU frame" and this emulator passes it, as it has every release. The claim came from a trace the ticket's own instrument-traps section says carries no CHR address column and therefore cannot see an A12 rise. A trace that could not see the event was read as evidence the event did not happen -- the second time here an instrument has been mistaken for its subject. THREE CLAIMS BECOME CHECKS. sys/ verbatim rested on one measurement taken eight releases ago and now pins 57 files, catching stray ones as well as changed and missing. The .qsf published two seed tables disagreeing about the pinned seed's margin by 0.155 ns. And bump_release.py, which dropped a release from the chain in two consecutive releases, turns out to RELOCATE it thirty entries away rather than drop it -- reproduced in a worktree, fixed, and its CHAIN rewrite extracted into a function because the selftest covering it re-implemented the substitution inline, so the one path that had gone wrong twice was the one nothing executed. AN ACCURACY GATE SOMEONE ELSE CAN RUN. The nine rung-1 ROMs export from a PINNED oracle commit and compare in CI. A subset, and the job's name says so. ONE FLAGGED CLAIM WAS NOT STALE, and saying so is the discipline: the sibling's ledger changelog was correct, and needed only for its silence to be legible. VERIFIED, NOT ASSERTED, for the parts that can be. The emulation core is unchanged -- the mmc3.rs edits are #[ignore] reasons -- so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction, and the mmc3 verdicts are byte-identical to the baseline captured before any edit. fmt clean; clippy clean on --workspace and on all three frontend feature combos; rustdoc built under -D warnings; the no_std cross-compile builds; 2,261 workspace tests pass with 0 failures; and all five release audits are green. NOT CLAIMED. No hardware has run the bitstream. Rung 6 stays open, confirmed by checking rather than assumed, and by maintainer decision v2.7.0 now WAITS for a board rather than submitting a core nothing has run. docs/bringup.md is written so that when one arrives, rung 6 costs a session. * fix(review): six findings from PR #494, one of them a defect in a gate I added FIVE ACCEPTED, ONE PARTLY, and one reviewer diagnosis was wrong while pointing at a real defect anyway. bump_release.py: the refusal did not fail closed on the DRY RUN. The dry-run branch returns 0 before the chains_needing_a_summary check could run, so --apply-less validation -- which is how a release is rehearsed -- reported success over a chain still owed a written summary. The check now runs before either exit path and both modes print the same refusal. Demonstrated in a worktree: the dry run exits 1, names the chain, and writes nothing. The reviewer also proposed moving the check ahead of the WRITES in apply mode. Declined, with the reason recorded at the site: a chain entry names the NEW release, so it cannot be written before the bump that creates it, and refusing to write until the chain is complete would make the chain unwritable. The exit code is the signal; the anchors are what was wanted. The dry-run half of the finding is right and is fixed; the apply half is not. ADR 0002: "never reachable by any lever on any axis this ADR searched" is narrowed. They ARE reachable -- by a lever the ADR never considered because it is not about timing at all, the assertion semantics of clock_irq's $C001-reload path, which this very document then exercises and rejects ON ITS COST. Claiming impossibility while demonstrating the opposite two sections later is the kind of overclaim this release exists to find. T-ORACLE-001's owner-facing summary still said "the mechanism is known, the fix is known" directly above the retraction disproving both. Rewritten. It also now records the DIRECTION, which rules out the second half of the proposed fix independently: registering /IRQ makes the assertion LATER, and this residual is already late, so whatever closes it must move the assertion earlier. The v2.6.15 plan document still listed workstream A as a core change and set an acceptance criterion of "AccuracyCoin verified, not asserted". The core did not change, so it holds by construction and that criterion does not apply. Corrected in place rather than rewritten, with the original wording quoted, because a plan that disagrees with the release it plans is the defect this version is named for. IMPLEMENTATION_PLAN.md's PPU row still said sprite evaluation was in progress. Rung 3 closed at v2.5.8. Missed by my own sweep of that table, which corrected the five rows below it and read past the one above. AND ONE WHERE THE DIAGNOSIS WAS WRONG. The reviewer read SUPPORT.md as attributing "the claims become checkable" to v2.6.14. It does not -- v2.6.15 carries that and v2.6.14 carries the checklist audit, correctly. But the line DOES have a defect the reading found: my earlier pass lowercasing the release lead matched only the em-dash form, so the parenthesised and comma forms kept a capital mid-sentence, in three files. Verified before fixing, per this project's own rule about reviewer claims; fixed the thing that was actually wrong. Anchor audit 13/13 and markdownlint clean after all six. * docs(release): record two findings the release build itself produced Neither was planned. Both came out of compiling the bitstream for this release, and both are the shape the version is named for -- a property that was measured once, under conditions nobody wrote down. THE BUILD IS REPRODUCIBLE AND THE REPRODUCIBILITY IS DAY-SCOPED. v2.6.15 changes no RTL and its bitstream came out 56,680 bytes larger with the timing at every corner moved. sys/build_id.tcl is a pre-flow script that rewrites build_id.v with the calendar date on every compile, and rtl/emu.sv puts BUILD_DATE into CONF_STR, so the date is a constant IN THE DESIGN and two builds on different days are different designs. Rebuilding with build_id.v pinned to 260903 reproduces the published artifact exactly -- md5 2c2fa6eb..., 4,040,572 bytes -- while 260904 gives 7346a490... at 4,097,252. Six characters, 56,680 bytes: the .rbf is compressed, so its size tracks placement rather than the device. v2.6.7's reproducibility result stands and its scope was never recorded. It also weakens what v2.6.14 claimed -- that its bitstream was "byte-identical to v2.6.13's ... an identical artifact demonstrates it" -- because that was achieved by RENAMING the file rather than rebuilding, and a rebuild on a different day would not have been identical, through no fault of the RTL. THE BUILD ALSO REWROTE THE PROJECT FILE. One compile appended 218 lines to the 281-line .qsf -- 145 pin assignments, 62 instance assignments, 3 HPS locations and 8 globals, every one already supplied by the vendored sys/ Tcl. The Template Readme warns about exactly this in the second person, and nothing here was watching: git status showed the file modified, which is what it always shows after a release edit, so the pollution was indistinguishable from the intended change until the diff was read. Both fixes live in the sibling -- the filename becomes a reproduction key, and the .qsf checker refuses framework-owned assignments with three more mutations. Recorded here because the CHANGELOG and the release notes are where a user learns what a release did. * docs: the suite is 147, measured, and two figures are deliberately NOT rescaled The full co-simulation suite was run from a frozen copy after the five cpu_interrupts_v2 verdict gates landed: TOTAL 147 passed, 0 failed, 0 skipped. 142 + 5, and all five report blargg verdict $00. The counts that describe the CURRENT suite move to 147 -- the sibling README, docs/bringup.md, tb/fetch-goldens.sh, and this release's own entries here and in docs/mister.md, which now say 142 at the version open and 147 at its close rather than picking one. TWO FIGURES ARE LEFT AT 142 ON PURPOSE, and scoped instead of rescaled. docs/sdram.md and docs/rung7-mappers.md record that the OFF-DIE build passes 140 of 142 gates. That was measured at v2.6.13 against the suite as it then stood, and the off-die build has not been re-run since. Writing "145 of 147" would be arithmetic dressed as a measurement -- the five new gates have never been run in that configuration, and whether they pass there is unknown. Both sites now name the suite the figure belongs to and say the off-die build has not been re-run. That distinction is the whole of the release's subject applied to its own numbers: a count that describes something measured, and a count that would describe something nobody produced, are different kinds of statement and must not be edited by the same pass. The first attempt to run the suite exited immediately -- the frozen copy lived in the scratchpad, and regress.sh resolves its repo root from its own dirname, so it looked for goldens beside the copy. That trap is already recorded in this project's session notes; re-running from tb/ with a per-run name is the recorded fix, and the script was removed afterwards.
Bumps softprops/action-gh-release from 1 to 2.
Release notes
Sourced from softprops/action-gh-release's releases.
Changelog
Sourced from softprops/action-gh-release's changelog.
Commits
a06a81arelease 2.5.07da8983feat: mark release as draft until all artifacts are uploaded (#692)8797328chore(deps): bump actions/checkout in the github-actions group (#689)1bfc62achore(deps): bump the npm group across 1 directory with 5 updates (#697)5be0e66release 2.4.2af658b4feat: Ensure generated release notes cannot be over 125000 characters (#684)237aaccchore: bump node to 24.11.000362bechore(deps): bump the npm group with 5 updates (#687)0adea5achore(deps): bump the npm group with 3 updates (#686)aa05f9dchore(deps): bump actions/setup-node from 5.0.0 to 6.0.0 in the github-action...Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)