Skip to content

chore(ci): Bump softprops/action-gh-release from 1 to 2 - #3

Merged
doublegate merged 3 commits into
mainfrom
dependabot/github_actions/softprops/action-gh-release-2
Dec 19, 2025
Merged

chore(ci): Bump softprops/action-gh-release from 1 to 2#3
doublegate merged 3 commits into
mainfrom
dependabot/github_actions/softprops/action-gh-release-2

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Dec 19, 2025

Copy link
Copy Markdown
Contributor

Bumps softprops/action-gh-release from 1 to 2.

Release notes

Sourced from softprops/action-gh-release's releases.

v2.0.0

  • update actions.yml declaration to node20 to address warnings
Changelog

Sourced from softprops/action-gh-release's changelog.

0.1.12

  • fix bug leading to empty strings substituted for inputs users don't provide breaking api calls #144
Commits
  • a06a81a release 2.5.0
  • 7da8983 feat: mark release as draft until all artifacts are uploaded (#692)
  • 8797328 chore(deps): bump actions/checkout in the github-actions group (#689)
  • 1bfc62a chore(deps): bump the npm group across 1 directory with 5 updates (#697)
  • 5be0e66 release 2.4.2
  • af658b4 feat: Ensure generated release notes cannot be over 125000 characters (#684)
  • 237aacc chore: bump node to 24.11.0
  • 00362be chore(deps): bump the npm group with 5 updates (#687)
  • 0adea5a chore(deps): bump the npm group with 3 updates (#686)
  • aa05f9d chore(deps): bump actions/setup-node from 5.0.0 to 6.0.0 in the github-action...
  • Additional commits viewable in compare view

Dependabot compatibility score

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 rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will 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 version will 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 dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

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>
@dependabot @github

dependabot Bot commented on behalf of github Dec 19, 2025

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: dependencies, github-actions. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@dependabot
dependabot Bot requested a review from doublegate as a code owner December 19, 2025 02:18
@doublegate
doublegate requested review from Copilot and removed request for doublegate December 19, 2025 05:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 doublegate left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed / Approved - GitHub Version Bump(s) -- DG 12/19

@doublegate
doublegate merged commit 07883e2 into main Dec 19, 2025
11 checks passed
@doublegate
doublegate deleted the dependabot/github_actions/softprops/action-gh-release-2 branch December 19, 2025 06:08
doublegate added a commit that referenced this pull request Jun 16, 2026
…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>
doublegate added a commit that referenced this pull request Jun 17, 2026
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>
doublegate added a commit that referenced this pull request Jun 17, 2026
…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>
@doublegate

Copy link
Copy Markdown
Owner

Adjudicated (post-hoc closeout): Copilot's review is an overview of a routine Dependabot bump — softprops/action-gh-release v1→v2 (Node 16→20), release workflow YAML only, no source / runtime / AccuracyCoin impact. Adopted + merged; no actionable review items outstanding.

doublegate added a commit that referenced this pull request Jun 19, 2026
fix(frontend): apply HD-pack tile substitution at render (align runtime key with real-Mesen format) (v1.7.1 #3)
doublegate added a commit that referenced this pull request Jul 2, 2026
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>
doublegate added a commit that referenced this pull request Jul 2, 2026
…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>
doublegate added a commit that referenced this pull request Jul 2, 2026
…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>
doublegate added a commit that referenced this pull request Jul 10, 2026
…(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>
doublegate added a commit that referenced this pull request Jul 11, 2026
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>
doublegate added a commit that referenced this pull request Jul 11, 2026
…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>
doublegate added a commit that referenced this pull request Jul 11, 2026
…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>
doublegate added a commit that referenced this pull request Jul 11, 2026
…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>
doublegate added a commit that referenced this pull request Jul 11, 2026
…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>
doublegate added a commit that referenced this pull request Jul 12, 2026
…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>
doublegate added a commit that referenced this pull request Jul 12, 2026
…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>
doublegate added a commit that referenced this pull request Jul 12, 2026
…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>
doublegate added a commit that referenced this pull request Aug 24, 2026
…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>
doublegate added a commit that referenced this pull request Sep 4, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants