diff --git a/.github/release-notes/v2.3.6.md b/.github/release-notes/v2.3.6.md new file mode 100644 index 00000000..a0a1a6c8 --- /dev/null +++ b/.github/release-notes/v2.3.6.md @@ -0,0 +1,97 @@ +RustyNES **v2.3.6 "Sounding"** is about measuring — and about what a measurement is allowed to claim. A *sounding* is a depth taken with its uncertainty attached, and that is what every workstream here has in common. Two features that shipped in earlier releases turn out never to have worked at all. Two new tools are added, both built to **decline to answer** rather than guess. And an optimization campaign is closed on the strength of three measured rejections and the mechanism that explains them. + +**The accuracy contract is verified, not asserted.** The emulation core gains one `const fn` getter and nothing else, but AccuracyCoin was re-run regardless: **141/141** on the authoritative RAM decoder, nestest 0-diff. + +## Two shipped features never worked + +### Pixel Provenance returned an empty report for effectively every user + +The v2.3.2 "Lucid" marquee — click a pixel, get its whole causal chain — has been non-functional since it shipped, for two independent reasons. + +**Run-ahead erased the record before the UI could read it.** Run-ahead defaults to 1, and its per-frame rollback cleared both provenance stores. That clear is correct for a save-state load and for netplay rollback. It was wrong here for a reason unrelated to the restore itself: run-ahead's rollback is the *last* thing before the frontend releases the emulator lock, so the panel's first opportunity to look was always **after** the wipe. It did not discard a stale timeline; it discarded the record for the frame on screen. + +**Clicking a pixel was never implemented.** The panel offered two coordinate spinboxes and no hit-test at all, while the docs and release notes said "point at" a pixel. + +The reason this survived four releases is worth stating: **two source comments and four documentation claims asserted the opposite of their own code.** One comment said the clear left "exactly the visible frame's writes — which is the timeline the user is looking at", two lines above code that emptied both stores completely. Prose asserting an intent is what stopped anyone checking whether the code met it. The corrections keep the wrong text, quoted, alongside the evidence that disproves it. + +### Duck Hunt could never score + +Duck Hunt's Zapper protocol is "see nothing for one frame, then a bright spot in the next". The light bit was sampled at end-of-frame, so a read during frame N returned frame N−1 — the probe was **exactly inverted** against the protocol the game relies on. A second defect had to go with it: the aperture sampled rows the beam had not finished painting. + +Score went 000000 → 000500. + +## The Latency Oracle + +**Tools → Analysis → Latency Oracle.** How many frames of input lag does *this* game have, and what run-ahead depth removes them? + +Every emulator makes finding that number a manual ritual — hold a direction, frame-advance until the sprite moves, subtract one. RetroArch documents exactly that procedure; RustyNES's own settings panel said only "1 fits most games". Nothing measured it. + +It measures it: replay the current moment twice, once with a button held and once with nothing pressed, and report the first frame that differs. On a deterministic core two replays of identical state can differ for exactly one reason, so the answer is a property of the ROM rather than of the run. + +**Being honest is the hard part**, because a latency number gets acted on: + +- `None` and `Some(0)` are different answers and are never collapsed. `Some(0)` means the game reacted on the next frame; `None` means the probe could not tell. +- Six buttons across three observables (framebuffer → audio → work RAM), requiring agreement. `START` is deliberately excluded: it pauses many games, which *is* a reaction — to a menu, not to gameplay — and counting it would over-report. +- Per-button evidence is shown even for confident results. A tool that publishes only its conclusion cannot be checked. +- **It recommends; it never applies.** Each extra frame of run-ahead costs roughly a whole frame of emulation, so raising it can drop frames on a marginal host. The depth appears with an explicit Apply button, and a test fails if storing a report ever queues a config write by itself. + +## The RAM Atlas + +**Tools → Analysis → RAM Atlas.** What is each byte of the 2 KiB work RAM *for*? + +Not "which addresses hold 42" — every emulator answers that, this one included, with RAM Search, RAM Watch and per-address access counts. Those narrow a set you already have a hypothesis about. None of them says what an address *is*, because observation alone cannot: an address counting up while the score counts up might be the score, or a frame counter that happens to be running. + +Two stages, with deliberately different confidence: + +- **Observation is correlation.** Every address is labelled untouched / frame tick / rising / falling / sparse / volatile. All 2048 labels come back marked **untested**, so observation is *structurally incapable* of claiming an effect. +- **Verification is a fact, and a narrow one.** Poke the byte, re-simulate from the same anchor, compare. + +The honesty properties are the design, not decoration: + +- **Liveness is relative to its lens**, and every verdict names the one it used. The same byte is routinely `Live` through work RAM and `Inert` through the framebuffer. +- **`Untested` is a third state**, distinct from `Inert`: "we did not look" and "we looked and saw nothing" are different claims. An unaffordable verification spends *zero* trials rather than a wasted baseline. +- **`Inert` is not "unused."** A byte the game rewrites from a master copy every frame reads inert because your change is overwritten before it can matter. +- **Every threshold is public**, so the panel can show "changed on 178 of 179 transitions, at or above the 90% frame-tick threshold" beside the label. A cutoff you cannot see is one you cannot disagree with. + +There is **no "verify everything" button**: verification costs two re-simulations per address, so all 2048 would be over four thousand runs and tens of minutes. The batch is capped at 16 and skips untouched addresses. + +## The menus are regrouped by task + +Tools had reached **twenty flat entries** spanning cheats, TAS authoring, media capture, multiplayer, ROM inspection and provenance analysis. Debug listed "CPU" and "Lua Script" as peers in a fifteen-item column. + +Tools is now Cheats at the top level, then Movies & Recording, Audio, Input, Game Data, Analysis and HD Pack — with Netplay and RetroAchievements below a separator, since they change what the *session* is rather than being tools you point at the game. Debug splits into Chip State, Memory and Execution, plus Symbols. Emulation's two FDS entries become one Famicom Disk System submenu. + +No entry is removed, none changes what it dispatches, and nothing moves between top-level menus — only the depth at which it sits. + +## APU Workstream D is closed + +The **18.7% of frame time** figure stands: it is a correct v2.3.1 attribution, visible only because that pass attributed by *source file*, since fat LTO inlines the APU into `cpu_clock` and hides it from a symbol profile. What is settled is narrower — the figure is **not recoverable by gating per-cycle bookkeeping**, the only strategy the workstream ever tried. + +One adoption (C1, shipped in v2.3.5 at −3.3% to −4.2%), three measured rejections, one declined on inspection, two left unmeasured deliberately. + +The three rejections share one cause, and it generalises: under `lto = "fat"` with `codegen-units = 1` the guarded code is already inlined, its repeated loads already merged, and the elided branches always-not-taken and perfectly predicted. The rule worth keeping — **"this work is inert on almost every cycle" predicts a win only if the work is actually *executed***, and under fat LTO with perfect prediction it largely is not. + +One measurement lesson is recorded because it will recur: D1's first A/B run looked like a textbook win at **−3.81% (p = 0.00)** on a shipped default workload and was **entirely an artifact**. The order-bias control, which benches the reference against *itself*, drifted −3.73% on that same workload with no code change at all — because the harness benchmarks the reference immediately after a 44.9-second fat-LTO compile across all cores, on a machine that has not cooled down. + +## Three defects the review of the RAM Atlas caught + +Worth naming, because all three are the kind that ship silently: + +- **The audio observable was structurally dead.** `Observable::AudioEnergy` never saw any audio — the trial loop emptied its buffer and never filled it, so every frame of every trial reported zero energy. Nothing failed, because *a lens that returns a constant never disagrees with itself*: the Latency Oracle's audio fallback silently degraded to work RAM, and the RAM Atlas's audio lens would have called **every address inert**. The comment above the missing call read "Drain EVERY frame, whatever the observable". +- **The Atlas was not gated during locked sessions.** Both actions advance the live emulator and Verify pokes memory, so under netplay or a TAS record/replay it would diverge a timeline other peers are lockstepped to, and under RetroAchievements hardcore it is the write that mode exists to forbid. It now reads the same predicate `emu.write` uses. +- **An address outside work RAM was reported `Inert`.** The poke was skipped, the two identical trials agreed, and a confident verdict was issued for a byte never touched — the precise failure mode that module's own documentation warns against. + +## Also in this release + +- **`rustynes-probe`**, the deterministic re-simulation engine both new tools consume: anchor, replay under controlled variation, locate the first divergence. Budgeted, and the budget is binding rather than advisory. +- **A probe trial no longer clears or pollutes the caller's rewind ring.** This is a bug an earlier fix *in this same release* reported as closed and did not close — it changed only the final restore, leaving every trial's restore untouched, so the ring was still being cleared 21 times per measurement. Fixing it revealed a second defect underneath: the ring then *grows*, with replayed frames that never happened on your timeline. Separately pinned: **no measurement taken before the fix needed re-running.** +- **Attestation tests** closing the last item of #360. +- **Docs**: specs for both new tools, a user-facing `Analysis tools` guide page, and a corrected menu reference. `docs/pixel-provenance.md` was also added to the docs-site nav, having been built but unreachable since v2.3.2. + +## Not fixed here + +- **`libretro/docs#1180`** — the licence correction on the libretro documentation site is still open upstream. +- **The upstream libretro `.info` still reads `v2.3.5`**, deliberately. RetroArch reads `dist/info/rustynes_libretro.info` from `libretro/libretro-super`, a separate copy nothing syncs automatically; upstream syncs are batched to MINOR releases, so the next one is v2.4.0. A stale `display_version` misreports a number; the thing that actually went wrong in v2.2.9 was a stale **licence**, and anything touching licence, supported extensions, or declared capabilities still syncs immediately regardless. +- **Latency Oracle and RAM Atlas persistence and export paths** — per-game persistence, and seeding the Watch/Cheat panels, the Lua API and RetroAchievements authoring from an atlas. Additive, and better shaped once the labels have been used in anger. + +Two upstream items *did* land since v2.3.5: `libretro-super#2069` (RetroArch now reads `GPLv3+`) and `RetroArch#19416`, which adds RustyNES to the Apple App Store core list — so iOS/iPadOS/tvOS availability arrives with the next App Store RetroArch build, on libretro's cadence. diff --git a/AGENTS.md b/AGENTS.md index 374bc208..d078f61b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Enforcement lives alongside the prose: `/ref-proj/` is gitignored/`.dockerignore RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in pure Rust. The accuracy bar is Mesen2 / higan / ares: tight lockstep scheduling at PPU-dot resolution on a master-clock-precise timebase, sub-instruction PPU events visible to subsequent CPU code, and a lookup-table non-linear audio mixer with band-limited synthesis. The frontend is pure Rust (`winit` + `wgpu` + `cpal` + `egui`). -**Current release: v2.3.5 "Manifest"** (2026-08-16) — about **what the core declares about itself**. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms. It does: RetroArch reads `dist/info/rustynes_libretro.info` from **`libretro/libretro-super`**, a SEPARATE copy from this repo's that nothing syncs and nothing compared, so the v2.2.9 GPL relicense never reached it (both upstream PRs merged 2026-07-21, exactly two weeks BEFORE the 2026-08-04 relicense). The repo-side half is corrected here — `GPLv3+`, since libretro uses short tokens and marks "or later" with a trailing `+` (tallied across all 316 upstream cores) — plus a standing `libretro_info_audit.rs` that pins the local file against the workspace manifest AND the core's own `retro_get_system_info`, making the upstream sync a **copy** rather than a re-derivation. **A licence change is now a mandatory upstream-sync trigger.** Auditing the wrapper then found **five further defects, every one with correct emulation behind it**: a hardcoded 60.0988 fps for every cartridge plus `retro_get_region` unimplemented (**PAL/Dendy ran 20.2% fast**), `retro_reset` unimplemented so **RetroArch's Reset did nothing, ever** (the library default is a literal no-op), `retro_unload_game` unimplemented (Game Genie *indices* leaked across cartridges), `aspect_ratio = 0.0` (square pixels, against the desktop frontend's 8:7), and no controller info so the **Zapper was unreachable** despite `Nes::set_zapper` being fully implemented. Review caught a **use-after-free**: RetroArch shallow-`memcpy`s the outer `retro_controller_info` array but RETAINS each `types` pointer, so the description tables must be `'static` (`SET_INPUT_DESCRIPTORS` is different and safe — never generalize between environment calls). The crate went from **zero tests to eight**. Separately the APU (**18.7% of frame time**, invisible to a symbol profile because fat LTO inlines it into `cpu_clock`) gained its first throughput bench and a default-configuration mix specialization, **−3.3% to −4.2%** on `nes_run_frame_nestest`, byte-identical by construction. Declared values are now DERIVED from `rustynes_core` constants (`FRAME_DURATION_*`, `DEFAULT_SAMPLE_RATE`) rather than transcribed. Audio stays **44,100 Hz** — a matched-normalized-frequency SFDR comparison shows 44.1k and 48k are equivalent (81.6 vs 82.2 dB), so nothing is gained, and 44,100 is the only rate this project's audio is verified at. Shipped OUTPUT byte-identical, but the APU *implementation* did change (the mix specialization is a strict specialization, not a no-op), so **AccuracyCoin 141/141 and nestest 0-diff were VERIFIED, not asserted**. **NOT fixed by this release:** RetroArch still shows the wrong licence until `libretro/libretro-super` + `libretro/docs` merge, and RustyNES still does not appear on iOS/iPadOS/tvOS (a hardcoded `appstore_cores` list in `libretro/RetroArch`, NOT a build failure — the buildbot has a valid `ios-arm64` core). Both land upstream, outside this project's control. +**Current release: v2.3.6 "Sounding"** (2026-08-17) — about **measuring, and what a measurement is allowed to claim**. **Two shipped features are found never to have worked.** Pixel Provenance (the v2.3.2 marquee) returned an empty report for every user on the default `run_ahead = 1`: run-ahead's per-frame rollback is the LAST thing before the frontend releases the emulator lock, so the panel's first look was always *after* the wipe — and "click any pixel" was **never implemented** (two `DragValue` spinboxes; the only `Sense` in the file was `hover()` on a colour swatch). **Two source comments and four doc claims asserted the opposite of their own code**, which is why four releases passed unchecked. And **Duck Hunt could never score**: its protocol is "see NOTHING for one frame, then a bright spot in the next", and the light bit was sampled at end-of-frame, so a read during frame N returned frame N-1 — the probe **exactly inverted** (000000 -> 000500). Two new tools, both built to **decline rather than guess**: the **Latency Oracle** (replays one moment with a button held and without it; `None` and `Some(0)` are different answers never collapsed; `START` excluded because pausing is a reaction to a *menu*; **recommends a run-ahead depth and never applies one**) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then VERIFIES a candidate by perturbing it — observation returns all 2048 labels as `Untested` so it is *structurally* incapable of claiming an effect; liveness is relative to its lens and every verdict names it; `Inert` is documented as NOT meaning unused). **APU Workstream D is CLOSED** — the 18.7%-of-frame figure stands, but it is not recoverable by gating per-cycle bookkeeping: one adoption, three measured rejections, one declined on inspection, two left unmeasured deliberately. Tools and Debug are regrouped by task (Tools had reached twenty flat entries). The core gains one `const fn` getter and nothing else, so **AccuracyCoin 141/141 and nestest 0-diff are VERIFIED, not asserted**. **NOT fixed here:** `libretro/docs#1180` (the licence on the libretro docs site) is still open upstream. Built on **v2.3.5 "Manifest"** (2026-08-16) — about **what the core declares about itself**. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms. It does: RetroArch reads `dist/info/rustynes_libretro.info` from **`libretro/libretro-super`**, a SEPARATE copy from this repo's that nothing syncs and nothing compared, so the v2.2.9 GPL relicense never reached it (both upstream PRs merged 2026-07-21, exactly two weeks BEFORE the 2026-08-04 relicense). The repo-side half is corrected here — `GPLv3+`, since libretro uses short tokens and marks "or later" with a trailing `+` (tallied across all 316 upstream cores) — plus a standing `libretro_info_audit.rs` that pins the local file against the workspace manifest AND the core's own `retro_get_system_info`, making the upstream sync a **copy** rather than a re-derivation. **A licence change is now a mandatory upstream-sync trigger.** Auditing the wrapper then found **five further defects, every one with correct emulation behind it**: a hardcoded 60.0988 fps for every cartridge plus `retro_get_region` unimplemented (**PAL/Dendy ran 20.2% fast**), `retro_reset` unimplemented so **RetroArch's Reset did nothing, ever** (the library default is a literal no-op), `retro_unload_game` unimplemented (Game Genie *indices* leaked across cartridges), `aspect_ratio = 0.0` (square pixels, against the desktop frontend's 8:7), and no controller info so the **Zapper was unreachable** despite `Nes::set_zapper` being fully implemented. Review caught a **use-after-free**: RetroArch shallow-`memcpy`s the outer `retro_controller_info` array but RETAINS each `types` pointer, so the description tables must be `'static` (`SET_INPUT_DESCRIPTORS` is different and safe — never generalize between environment calls). The crate went from **zero tests to eight**. Separately the APU (**18.7% of frame time**, invisible to a symbol profile because fat LTO inlines it into `cpu_clock`) gained its first throughput bench and a default-configuration mix specialization, **−3.3% to −4.2%** on `nes_run_frame_nestest`, byte-identical by construction. Declared values are now DERIVED from `rustynes_core` constants (`FRAME_DURATION_*`, `DEFAULT_SAMPLE_RATE`) rather than transcribed. Audio stays **44,100 Hz** — a matched-normalized-frequency SFDR comparison shows 44.1k and 48k are equivalent (81.6 vs 82.2 dB), so nothing is gained, and 44,100 is the only rate this project's audio is verified at. Shipped OUTPUT byte-identical, but the APU *implementation* did change (the mix specialization is a strict specialization, not a no-op), so **AccuracyCoin 141/141 and nestest 0-diff were VERIFIED, not asserted**. **NOT fixed by that release, and since RESOLVED upstream:** RetroArch showed the wrong licence until `libretro-super#2069` merged (2026-08-16 — it now reads `GPLv3+`), and RustyNES did not appear on iOS/iPadOS/tvOS until `RetroArch#19416` merged (2026-08-16, `76f60626984a` — `rustynes` is now line 268 of `pkg/apple/update-cores.sh`, between `reminiscence` and `sameboy`). Being in the build list is not the same as being installable: it arrives with the next App Store RetroArch build, on libretro's cadence. Only `libretro/docs#1180` remains open. The prior release, **v2.3.4 "Ledger"** (2026-08-15), was the **coverage** release. Three boards land: **mapper 176 submapper 2** (WAIXING-FS005 — the `$A001` RAM Configuration Register with 32 KiB banked WRAM, the `$5000-$5FFF` register-window disable the Waixing copy-protection is built on, a mapper-195-like mixed CHR-ROM/CHR-RAM mode, two-bit `$A000` mirroring, the `$46`/`$47` bank-select swap that does NOT apply to `$06`/`$07`, PRG A21-A25, and the board's documented `$E003` decode mask), **154** (NAMCOT-3453 — mapper 88 plus a one-screen nametable bit decoded across the WHOLE `$8000-$FFFF` range, not just the bank-select window) and **243** (Sachen SA-020A — mapper 150's ASIC on its own PCB, same three registers at INVERTED significance, which is why they need separate numbers). Breadth **172 → 174 families** (51 Core + 95 Curated + 28 BestEffort). All three implemented from the NESdev wiki with **no reference-emulator source consulted**, unlike the FK23C transforms beside them which stay a disclosed Mesen2 derivation. @@ -202,7 +202,7 @@ These cross-cutting decisions span multiple files. Reading individual chip docs - `ref-docs/` is immutable. Research updates go in dated supplemental files. - ADRs go in `docs/adr/` (Michael Nygard format). - `rustynes-core` re-exports the public types from the chip crates; downstream consumers (`rustynes-frontend`, `rustynes-test-harness`) should depend on `rustynes-core` rather than the chip crates directly. -- When relabeling old engine "v2.x" narrative for users, present it as upstream lineage/history — **never as a current RustyNES release version.** The current release is **v2.3.5 "Manifest"** (2026-08-16, the declaration release — the libretro `.info` RetroArch reads is a SEPARATE upstream copy the GPL relicense never reached, corrected to `GPLv3+` with a standing audit; five wrapper defects each with correct emulation behind them [PAL 20.2% fast, Reset inert, unload leaked cheat indices, square-pixel aspect, Zapper unreachable]; a use-after-free in the controller tables found in review; the APU's first throughput bench + a −3.3%/−4.2% default-mix specialization; AccuracyCoin 141/141 VERIFIED. The RetroArch licence display and iOS/iPadOS/tvOS availability both remain blocked on upstream PRs), on top of **v2.3.4 "Ledger"** (2026-08-15, the coverage release — mappers 176/2 (WAIXING-FS005), 154 (NAMCOT-3453) and 243 (Sachen SA-020A) taking breadth to 174 families; the coverage harness moved onto the frontend's real load path, exposing a per-game-database defect that had made every Sachen cartridge unloadable since v1.2.0; this one TOUCHES the core, so AccuracyCoin 141/141 is verified, not by construction; Workstream C — the APU at 18.7% — was NOT delivered and is carried to v2.3.5), on top of **v2.3.3 "Cadence"** (2026-08-14, the display-pacing release — the run-ahead throttle oscillation traced to a stale median, a predictive engage arm, and the `wp_presentation` apparatus; frontend-only, AccuracyCoin 141/141), on top of **v2.3.2 "Lucid"** (2026-08-11, the pixel-provenance release — per-byte write attribution + the per-pixel causal record + the Tools → Pixel Provenance panel + deterministic replay attestation via `rustynes verify`; all `debug-hooks`-gated and output-only, so AccuracyCoin holds 141/141 and nestest is 0-diff), on top of **v2.3.1 "Plumb Line"** (2026-08-06, the measurement release — ten hot-path candidates measured and all ten rejected), itself on **v2.3.0 "Datum II"** (2026-08-05, the capstone closing the v2.2.6 → v2.3.0 NESdev-remediation line — **true multi-viewport OS-window detach** for every tool panel (v2.2.9's affordance only *embedded* them, so the Windows-10 trapped-window report is now genuinely fixed); a **frame-pacing fix** predating that work (the render path held the emulator lock across the blocking swapchain acquire + present, stalling frame production whenever a debugger panel was open — now split so the lock covers only the egui UI build, plus `pace_frames` reading a lock-free `has_rom` atomic instead of locking every `about_to_wait`); a **−5.13% / −3.51%** byte-identical PPU optimization (`v2.3.0 P1`: `#[inline]` on the per-dot sprite eval + hoisting the `tick_oam_bus` early-out); both remaining forum-reported accuracy items (SMB left edge, Rad Racer hybrid-address) **verified already-correct**; and the AccuracyCoin gate pinned to an **exact 141/141**), on top of **v2.2.9 "Studio II"** (2026-08-04, a frontend quality-of-life release — TAStudio piano-roll edits wired to the emulator, `.bk2` playback honoring the movie's `LogKey` column order, and a detach/pop-out affordance for tool windows (the shared `detachable_window` helper across 18 panels) [native-only; it **embedded** the panel on the single-viewport `egui_winit` integration rather than opening a separate OS window — **resolved in v2.3.0** by the real multi-viewport implementation]; frontend-only so the deterministic core is untouched and AccuracyCoin holds 141/141, nestest 0-diff), on top of **v2.2.8 "Aperture II"** (2026-08-04, a presentation-fidelity release — gamma-correct scanlines + a WebGL2 gamma fix + a sharper scanline profile; presentation-only so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical, native default unchanged; visual verification pending), on top of **v2.2.7 "Timbre II"** (2026-08-04, an expansion-audio fidelity release — VRC6 recalibrated to ~1.0× a 2A03 pulse per the NESdev/field consensus [`VRC6_MIX_SCALE` 979→650; Mesen2's ~1.5× was the loud outlier], and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC; expansion-only, so the base 2A03 is byte-identical and AccuracyCoin holds 141/141), on top of **v2.2.6 "Almanac"** (2026-08-04, a de-monetization + provenance release — RustyNES is permanently open-source and income-free per ADR 0035; all planned monetization removed, native apps kept as free FOSS apps, and the TriCNES hybrid-address timing-calibration caveat disclosed per ADR 0030 for a v2.3.0 rework; zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction), on top of **v2.2.5 "Colophon"** (2026-08-03, a provenance/licensing/documentation-integrity release — zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction; `NOTICE` rewritten for full attribution + GPL-oracle disclosure + GeraNES, in-source "port" comments reworded to the oracle framing, the CRT-shader/NTSC provenance reworded to independent reimplementations, `docs/originality-and-provenance.md` added, README AI-assistance disclosure), on top of **v2.2.4 "Cartridge"** (2026-07-24, a libretro/RetroArch distribution cut — zero emulation-core changes so AccuracyCoin holds 141/141 by construction; the libretro core is confirmed up-to-date with all recent changes and builds for the buildbot ABIs [`x86_64-pc-windows-gnu`, `aarch64-linux-android`], and `rustynes_libretro.info` is corrected: `disk_control` false→true [the FDS Disk Control interface was wired but advertised absent], `display_version` v1.0.0→v2.2.4, mapper count 168→172; core options remain a documented future enhancement; the Antigravity reviewer standardization rides along), on top of **v2.2.3 "Datum"** (2026-07-23, a performance and accuracy-closure patch — the fast PPU dot path promoted to default and exposed, PGO binaries shipped on the release path, a same-runner relative frame-time CI gate, the last two Holy Mapperel residuals closed [MMC1 WRAM write-protect + FME-7 open bus, all 17 ROMs now `detail=0000`], the Sunsoft 5B level calibrated with `Mapper::mix_audio` widened to i32, a save-state schema gap fixed at `PPU_SNAPSHOT_VERSION` 8 + an APU v4 tail, an opt-in Zapper beam-relative light model, and the eleven `sprintN.rs` mapper modules renamed to `mNNN_.rs`; two optimizations measured and REJECTED and documented as such; AccuracyCoin 141/141 — on top of **v2.2.2 "Conduit"** [2026-07-21, a build/distribution/CI-integrity patch — the libretro buildbot recipe taken from 1 of 10 jobs green to all ten building, a GitHub Actions supply-chain hardening pass, and the toolchain collapsed to one pinned source of truth with no `nightly` on any build path; zero emulation-core changes], itself on **v2.2.1** [2026-07-15, a housekeeping patch: dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition], itself on **v2.2.0 "Capstone"** [2026-07-12], the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run — its two remaining marquees the netplay matchmaking / lobby stack and the FDS medium model, atop a peripherals + quality/security pass (Famicom `$4016`-bit-2 microphone + 3×3-aperture Zapper; cargo-fuzz targets 3 → 8 finding + fixing two `Movie::deserialize` OOM-DoS paths; a read-only Tools → ROM Info browser); every change additive or default-off, AccuracyCoin 141/141) on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. The v2.0.x "Harbor" mobile-finalization train (v2.0.1→v2.0.9) and the entire v2.1.x "Fathom" line (v2.1.0→v2.1.10) plus the v2.2.0 "Capstone" milestone have all shipped — the run's steps being v2.1.5 "Vernier" (regression-net & residual) → v2.1.6 "Timbre" (expansion-audio fidelity) → v2.1.7 "Stepping" (opt-in PPU/2A03 die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier a documented no-op on every oracle, ADR 0033) → v2.1.8 "Tempo" (a default-OFF fast PPU dot path + SIMD blitter + wasm size pass) → v2.1.9 "Aperture" (a marquee CRT shader stack + raw NTSC composite signal-decode + GIF/WAV capture + palette editor) → v2.1.10 "Loom" (TAStudio greenzone + Lua API breadth + browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation) → v2.2.0 "Capstone" (the milestone cut closing the run) → v2.2.1 (housekeeping) → **v2.2.2 "Conduit"** the build/distribution/CI-integrity patch — preceded by v1.10.0 "Arcade" the native Libretro / RetroArch core, the v1.9.0→v1.9.9 iOS TestFlight train, the v1.8.0→v1.8.9 "Android" train, and the desktop-feature lineage v1.1.0→v1.7.1, all on the v1.0.0 production core (see the top "Current release" block + `docs/STATUS.md`). **Never claim any version *later* than v2.3.5 is released** — the **v2.2.6 → v2.3.0** line (de-monetization + NESdev remediation: audio [v2.2.7, shipped], video/gamma [v2.2.8, shipped], TAS/UX [v2.2.9, shipped], and the PPU left-edge + hybrid-address accuracy capstone at **v2.3.0** "Datum II" [shipped]) is now **complete**. The freed **v2.3.0** slot is repurposed as that accuracy capstone (NOT a store launch — RustyNES is now income-free per ADR 0035; any free mobile-app store listing is a later, unversioned step with no monetization — see `to-dos/ROADMAP.md`). Two distinct "v2.0"s exist and must not be conflated, **both now shipped, at different times, for different reasons**: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) — it was the *only* scheduler through v1.10.0. RustyNES's own **v2.0.0 "Timebase"** release (2026-07-03) is a *different* milestone that *replaces* that same dot-lockstep scheduler outright: the **one-clock + every-cycle-bus-access collapse** (a single canonical cycle counter + a split-around-the-access `start_cycle`/`end_cycle` PPU catch-up, mirroring Mesen2's structure), full Vs. `DualSystem` dual-console emulation (core-and-harness-only; frontend wiring deferred), and the breaking save-state / cross-version changes it entailed (ADR 0002 / ADR 0028 / ADR 0029) — the one release that broke byte-identity / save-state compatibility, by design. The R1/R2 hard-tier MMC3 IRQ-timing residual was investigated under a bounded-effort campaign and is by-design-deferred beyond v2.0.0, not closed — see ADR 0002's decision-update section for the mechanism-level finding. +- When relabeling old engine "v2.x" narrative for users, present it as upstream lineage/history — **never as a current RustyNES release version.** The current release is **v2.3.6 "Sounding"** (2026-08-17, the measurement release — two shipped features found never to have worked [Pixel Provenance's record wiped by run-ahead before any UI could read it, its click never implemented; the Duck Hunt Zapper probe exactly inverted], the Latency Oracle and RAM Atlas both built to decline rather than guess, APU Workstream D closed on three measured rejections, and the Tools/Debug menus regrouped by task; core gains one `const fn` getter so AccuracyCoin 141/141 is VERIFIED), on top of **v2.3.5 "Manifest"** (2026-08-16, the declaration release — the libretro `.info` RetroArch reads is a SEPARATE upstream copy the GPL relicense never reached, corrected to `GPLv3+` with a standing audit; five wrapper defects each with correct emulation behind them [PAL 20.2% fast, Reset inert, unload leaked cheat indices, square-pixel aspect, Zapper unreachable]; a use-after-free in the controller tables found in review; the APU's first throughput bench + a −3.3%/−4.2% default-mix specialization; AccuracyCoin 141/141 VERIFIED. The RetroArch licence display and iOS/iPadOS/tvOS availability both remain blocked on upstream PRs), on top of **v2.3.4 "Ledger"** (2026-08-15, the coverage release — mappers 176/2 (WAIXING-FS005), 154 (NAMCOT-3453) and 243 (Sachen SA-020A) taking breadth to 174 families; the coverage harness moved onto the frontend's real load path, exposing a per-game-database defect that had made every Sachen cartridge unloadable since v1.2.0; this one TOUCHES the core, so AccuracyCoin 141/141 is verified, not by construction; Workstream C — the APU at 18.7% — was NOT delivered and is carried to v2.3.5), on top of **v2.3.3 "Cadence"** (2026-08-14, the display-pacing release — the run-ahead throttle oscillation traced to a stale median, a predictive engage arm, and the `wp_presentation` apparatus; frontend-only, AccuracyCoin 141/141), on top of **v2.3.2 "Lucid"** (2026-08-11, the pixel-provenance release — per-byte write attribution + the per-pixel causal record + the Tools → Pixel Provenance panel + deterministic replay attestation via `rustynes verify`; all `debug-hooks`-gated and output-only, so AccuracyCoin holds 141/141 and nestest is 0-diff), on top of **v2.3.1 "Plumb Line"** (2026-08-06, the measurement release — ten hot-path candidates measured and all ten rejected), itself on **v2.3.0 "Datum II"** (2026-08-05, the capstone closing the v2.2.6 → v2.3.0 NESdev-remediation line — **true multi-viewport OS-window detach** for every tool panel (v2.2.9's affordance only *embedded* them, so the Windows-10 trapped-window report is now genuinely fixed); a **frame-pacing fix** predating that work (the render path held the emulator lock across the blocking swapchain acquire + present, stalling frame production whenever a debugger panel was open — now split so the lock covers only the egui UI build, plus `pace_frames` reading a lock-free `has_rom` atomic instead of locking every `about_to_wait`); a **−5.13% / −3.51%** byte-identical PPU optimization (`v2.3.0 P1`: `#[inline]` on the per-dot sprite eval + hoisting the `tick_oam_bus` early-out); both remaining forum-reported accuracy items (SMB left edge, Rad Racer hybrid-address) **verified already-correct**; and the AccuracyCoin gate pinned to an **exact 141/141**), on top of **v2.2.9 "Studio II"** (2026-08-04, a frontend quality-of-life release — TAStudio piano-roll edits wired to the emulator, `.bk2` playback honoring the movie's `LogKey` column order, and a detach/pop-out affordance for tool windows (the shared `detachable_window` helper across 18 panels) [native-only; it **embedded** the panel on the single-viewport `egui_winit` integration rather than opening a separate OS window — **resolved in v2.3.0** by the real multi-viewport implementation]; frontend-only so the deterministic core is untouched and AccuracyCoin holds 141/141, nestest 0-diff), on top of **v2.2.8 "Aperture II"** (2026-08-04, a presentation-fidelity release — gamma-correct scanlines + a WebGL2 gamma fix + a sharper scanline profile; presentation-only so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical, native default unchanged; visual verification pending), on top of **v2.2.7 "Timbre II"** (2026-08-04, an expansion-audio fidelity release — VRC6 recalibrated to ~1.0× a 2A03 pulse per the NESdev/field consensus [`VRC6_MIX_SCALE` 979→650; Mesen2's ~1.5× was the loud outlier], and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC; expansion-only, so the base 2A03 is byte-identical and AccuracyCoin holds 141/141), on top of **v2.2.6 "Almanac"** (2026-08-04, a de-monetization + provenance release — RustyNES is permanently open-source and income-free per ADR 0035; all planned monetization removed, native apps kept as free FOSS apps, and the TriCNES hybrid-address timing-calibration caveat disclosed per ADR 0030 for a v2.3.0 rework; zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction), on top of **v2.2.5 "Colophon"** (2026-08-03, a provenance/licensing/documentation-integrity release — zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction; `NOTICE` rewritten for full attribution + GPL-oracle disclosure + GeraNES, in-source "port" comments reworded to the oracle framing, the CRT-shader/NTSC provenance reworded to independent reimplementations, `docs/originality-and-provenance.md` added, README AI-assistance disclosure), on top of **v2.2.4 "Cartridge"** (2026-07-24, a libretro/RetroArch distribution cut — zero emulation-core changes so AccuracyCoin holds 141/141 by construction; the libretro core is confirmed up-to-date with all recent changes and builds for the buildbot ABIs [`x86_64-pc-windows-gnu`, `aarch64-linux-android`], and `rustynes_libretro.info` is corrected: `disk_control` false→true [the FDS Disk Control interface was wired but advertised absent], `display_version` v1.0.0→v2.2.4, mapper count 168→172; core options remain a documented future enhancement; the Antigravity reviewer standardization rides along), on top of **v2.2.3 "Datum"** (2026-07-23, a performance and accuracy-closure patch — the fast PPU dot path promoted to default and exposed, PGO binaries shipped on the release path, a same-runner relative frame-time CI gate, the last two Holy Mapperel residuals closed [MMC1 WRAM write-protect + FME-7 open bus, all 17 ROMs now `detail=0000`], the Sunsoft 5B level calibrated with `Mapper::mix_audio` widened to i32, a save-state schema gap fixed at `PPU_SNAPSHOT_VERSION` 8 + an APU v4 tail, an opt-in Zapper beam-relative light model, and the eleven `sprintN.rs` mapper modules renamed to `mNNN_.rs`; two optimizations measured and REJECTED and documented as such; AccuracyCoin 141/141 — on top of **v2.2.2 "Conduit"** [2026-07-21, a build/distribution/CI-integrity patch — the libretro buildbot recipe taken from 1 of 10 jobs green to all ten building, a GitHub Actions supply-chain hardening pass, and the toolchain collapsed to one pinned source of truth with no `nightly` on any build path; zero emulation-core changes], itself on **v2.2.1** [2026-07-15, a housekeeping patch: dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition], itself on **v2.2.0 "Capstone"** [2026-07-12], the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run — its two remaining marquees the netplay matchmaking / lobby stack and the FDS medium model, atop a peripherals + quality/security pass (Famicom `$4016`-bit-2 microphone + 3×3-aperture Zapper; cargo-fuzz targets 3 → 8 finding + fixing two `Movie::deserialize` OOM-DoS paths; a read-only Tools → ROM Info browser); every change additive or default-off, AccuracyCoin 141/141) on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. The v2.0.x "Harbor" mobile-finalization train (v2.0.1→v2.0.9) and the entire v2.1.x "Fathom" line (v2.1.0→v2.1.10) plus the v2.2.0 "Capstone" milestone have all shipped — the run's steps being v2.1.5 "Vernier" (regression-net & residual) → v2.1.6 "Timbre" (expansion-audio fidelity) → v2.1.7 "Stepping" (opt-in PPU/2A03 die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier a documented no-op on every oracle, ADR 0033) → v2.1.8 "Tempo" (a default-OFF fast PPU dot path + SIMD blitter + wasm size pass) → v2.1.9 "Aperture" (a marquee CRT shader stack + raw NTSC composite signal-decode + GIF/WAV capture + palette editor) → v2.1.10 "Loom" (TAStudio greenzone + Lua API breadth + browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation) → v2.2.0 "Capstone" (the milestone cut closing the run) → v2.2.1 (housekeeping) → **v2.2.2 "Conduit"** the build/distribution/CI-integrity patch — preceded by v1.10.0 "Arcade" the native Libretro / RetroArch core, the v1.9.0→v1.9.9 iOS TestFlight train, the v1.8.0→v1.8.9 "Android" train, and the desktop-feature lineage v1.1.0→v1.7.1, all on the v1.0.0 production core (see the top "Current release" block + `docs/STATUS.md`). **Never claim any version *later* than v2.3.6 is released** — the **v2.2.6 → v2.3.0** line (de-monetization + NESdev remediation: audio [v2.2.7, shipped], video/gamma [v2.2.8, shipped], TAS/UX [v2.2.9, shipped], and the PPU left-edge + hybrid-address accuracy capstone at **v2.3.0** "Datum II" [shipped]) is now **complete**. The freed **v2.3.0** slot is repurposed as that accuracy capstone (NOT a store launch — RustyNES is now income-free per ADR 0035; any free mobile-app store listing is a later, unversioned step with no monetization — see `to-dos/ROADMAP.md`). Two distinct "v2.0"s exist and must not be conflated, **both now shipped, at different times, for different reasons**: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) — it was the *only* scheduler through v1.10.0. RustyNES's own **v2.0.0 "Timebase"** release (2026-07-03) is a *different* milestone that *replaces* that same dot-lockstep scheduler outright: the **one-clock + every-cycle-bus-access collapse** (a single canonical cycle counter + a split-around-the-access `start_cycle`/`end_cycle` PPU catch-up, mirroring Mesen2's structure), full Vs. `DualSystem` dual-console emulation (core-and-harness-only; frontend wiring deferred), and the breaking save-state / cross-version changes it entailed (ADR 0002 / ADR 0028 / ADR 0029) — the one release that broke byte-identity / save-state compatibility, by design. The R1/R2 hard-tier MMC3 IRQ-timing residual was investigated under a bounded-effort campaign and is by-design-deferred beyond v2.0.0, not closed — see ADR 0002's decision-update section for the mechanism-level finding. - **Forward plans + roadmap live in `to-dos/`.** `to-dos/ROADMAP.md` (updated in #129) is the planning entry point and frames the release line + "the path to v2.0.0 and beyond"; `to-dos/plans/` holds the per-release plan docs (through `v1.7.0-forge-plan.md` on `main`, plus the staged-forward `v1.8.0-android-plan.md` / `v1.9.0-ios-plan.md` / `v2.0.0-master-clock-plan.md`) + the `to-dos/plans/engine-lineage/` history archive + a `to-dos/plans/research/` reference-mining archive. - The v1.0.0 release + GitHub Pages/CI + post-release record is in `docs/v1.0.0-synthesis-handoff-2026-06-13.md` — read it before touching CI, Pages, or release tooling. Full per-release history is in `CHANGELOG.md`. - **Markdownlint is a CI gate** (pre-commit, pinned `markdownlint-cli v0.39.0`). The local `markdownlint` binary is a newer version that reports rules v0.39.0 lacks (e.g. MD060) — those are NOT gated; verify with `pre-commit run markdownlint --all-files`, not the bare binary. `.markdownlint.json` keeps `MD013`/`MD033`/`MD041` disabled by design (long technical tables, the README HTML banner/``, the HTML-led README). `.markdownlintignore` exempts `ref-docs/`, `ref-proj/` (the reference-emulator clone, now removed from disk but kept in the ignore lists as a firewall guard so it can never re-enter the tree — see the MOST IMPORTANT RULE section above), the vendored `tricnes/` + upstream READMEs, and the frozen `docs/archive/` + `to-dos/archive/` trees — don't lint or reformat those. @@ -226,8 +226,20 @@ These cross-cutting decisions span multiple files. Reading individual chip docs - **lz4_flex 0.14+ requires the crate's own `alloc` feature explicitly** for `compress_prepend_size`/`decompress_size_prepended` (used by `rewind.rs`/`zwinder.rs`) — it split real no_std support into an `alloc`-vs-`std` distinction that didn't exist in 0.13. A `cargo build --workspace` will NOT catch a missing `alloc` feature here because `rustynes-core`'s own default-on `std` feature implies it via cargo's feature unification; only a standalone `cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features` (the exact CI `no_std build` job) will. Run that command locally before pushing any bump that touches this dependency. - **The libretro `.info` RetroArch reads is a DIFFERENT FILE from this repo's, and it went stale for eleven days.** RetroArch downloads `dist/info/rustynes_libretro.info` from `libretro/libretro-super`; `crates/rustynes-libretro/rustynes_libretro.info` is an unrelated copy that nothing syncs and nothing compared. So the v2.2.9 GPL relicense reached `Cargo.toml`, `NOTICE`, `deny.toml`, the SPDX headers and the local `.info` — and **not** the file users actually see, which went on advertising "MIT OR Apache-2.0" at `display_version = v2.2.1`. Both upstream PRs had merged *exactly two weeks before* the relicense, so no sync could have carried it. **A license change is now a mandatory upstream-sync trigger**, on the same footing as a release. `crates/rustynes-test-harness/tests/libretro_info_audit.rs` pins the local file against the workspace manifest so the sync is a *copy*, never a re-derivation; it cannot see upstream, so the sync itself stays a human step. libretro `.info` uses short license tokens, not SPDX, and marks "or later" with a trailing `+` (tallied across all 316 upstream cores: `GPLv2` x100, `GPLv3` x64, `GPLv2+` x19, `GPLv3+` x5) — RustyNES is **`GPLv3+`**; a bare `GPLv3` understates it as GPL-3.0-only. Full detail + the surface table: `docs/libretro/UPSTREAM_SYNC.md`. -- **iOS/iPadOS/tvOS availability is a THIRD repo and a HARDCODED list — being on the buildbot buys nothing there.** iOS cannot download cores (Apple bans fetching executable code), so the App Store build bundles a fixed set chosen by `pkg/apple/update-cores.sh` in `libretro/RetroArch`. That script has two lists: `allcores`, fetched *dynamically* from the buildbot directory (RustyNES is in it automatically), and `appstore_cores`, a hardcoded array (RustyNES is **absent**). The iOS/tvOS build phases run `rm -f ${SRCROOT}//modules/*.dylib` then `./update-cores.sh appstore`, so only the hardcoded list survives. One entry covers iOS + tvOS + macOS App Store. **Alphabetical order is mandatory** — `rustynes` sorts between `reminiscence` and `sameboy`; re-check the neighbours at submission time rather than trusting a line number. +- **iOS/iPadOS/tvOS availability is a THIRD repo and a HARDCODED list — being on the buildbot buys nothing there.** **RESOLVED 2026-08-16** by `libretro/RetroArch#19416` (merged `76f60626984a`; verified against `master`, not the PR state — `rustynes` sits at line 268 between `reminiscence` and `sameboy`). The mechanism below is retained because it recurs for any other core and for the sibling forges, and because "in the build list" is not "installable": it ships with the next App Store RetroArch build. iOS cannot download cores (Apple bans fetching executable code), so the App Store build bundles a fixed set chosen by `pkg/apple/update-cores.sh` in `libretro/RetroArch`. That script has two lists: `allcores`, fetched *dynamically* from the buildbot directory (RustyNES is in it automatically), and `appstore_cores`, a hardcoded array (RustyNES is **absent**). The iOS/tvOS build phases run `rm -f ${SRCROOT}//modules/*.dylib` then `./update-cores.sh appstore`, so only the hardcoded list survives. One entry covers iOS + tvOS + macOS App Store. **Alphabetical order is mandatory** — `rustynes` sorts between `reminiscence` and `sameboy`; re-check the neighbours at submission time rather than trusting a line number. - **RetroArch retains SOME environment-callback pointers and copies others — the asymmetry is not documented in `libretro.h`, so check `runloop.c`.** `SET_CONTROLLER_INFO` shallow-`memcpy`s the outer `retro_controller_info` array but **retains** each entry's `types` pointer and dereferences it later when the Controls menu is built: the description arrays MUST be `'static` (a stack local compiles cleanly and hands the frontend a use-after-free). `SET_INPUT_DESCRIPTORS` is different — RetroArch walks it during the call and retains only the `description` string pointers — so a stack array is fine there. Never generalize from one to the other; read the handler. +- **A fix that touches ONE call site of a shared code path may not fix the bug — and will report that it did.** v2.3.6 hit this squarely. Review reported that a latency measurement destroyed the user's rewind history; the fix changed `measure_in_place`'s FINAL restore to `restore_quiet` and stopped. Every *trial* still went through `Probe::run_uncounted`'s loud `nes.restore(..)`, and a measurement runs up to **21 trials** against the live emulator, so the ring was still being cleared twenty-one times over behind a fix that closed the thread. Before declaring a fix complete, grep for every caller of the mechanism, not just the one the report named. The corollary is about tests: my test for that fix would have asserted "ring not empty", which **passes while a second defect remains** — the ring in fact GREW, polluted with replayed frames that never happened on the user's timeline. Assert the state comes back EXACTLY as it was; a weaker assertion is how an incomplete fix clears review. + +- **Prose asserting an intent is how a defect survives releases.** Pixel Provenance shipped broken for four releases with a comment two lines above the offending code claiming the clear "leaves exactly the visible frame's writes — which is the timeline the user is looking at", plus four matching doc claims. Nobody checked because the text said it was fine. When correcting this class, KEEP the wrong text quoted alongside the measurement that disproves it — deleting it loses the evidence that the prose was the mechanism. + +- **`ab_check.sh` benchmarks the reference immediately after building it, and on this workspace that is a ~45-second fat-LTO compile across all cores.** So the reference measures on a hot, frequency-throttled machine while the candidate runs once thermals have settled. v2.3.6 D1's run 1 read **-3.81% at p = 0.00** on a shipped default workload and was entirely artifact: the order-bias control, benching the reference against ITSELF, drifted **-3.73%** on that same workload with no code change. **Read the order-bias control before the candidate column.** A cached reference build plus `AB_MEASUREMENT_TIME=25` took the drift from ~4% to ~1%. + +- **"Inert on almost every cycle" predicts an optimization win only if the work is actually EXECUTED.** Under `lto = "fat"` with `codegen-units = 1` the guarded code is already inlined into its caller, its repeated loads already merged by common-subexpression elimination, and always-not-taken branches are perfectly predicted — so swapping predictable not-taken branches for an equivalent count of loads plus a predicate is arithmetically a wash. This is why APU Workstream D produced three nulls (D1, D3, D6) and why D2 and D4 are left unmeasured: their prior is a null, not an unknown. Full numbers and the three conditions that would justify reopening: `docs/performance.md`. + +- **The bot ceremony has a THIRD hiding place: plain issue comments.** `AGENTS.md` already warned that CodeRabbit and Copilot post suppressed findings in review BODIES, invisible to a resolve-every-thread sweep. The **Antigravity reviewer posts its entire review as an ordinary PR comment** — not a review, not a thread — so `gh pr view --json reviews` misses it too. On PR #385 its blocking finding (an emoji in code, against a hard project rule) was caught only because the comment list was read. Fetch all three: `reviewThreads`, `reviews[].body`, AND `comments[]`. Also note Antigravity re-reviews on every push, so a green build plus zero unresolved threads can still sit under an unread blocking finding — check the newest comment's timestamp against your last push. + +- **Panel state that outlives the `Nes` it describes is a recurring seam here.** Three instances now: the Pixel Provenance panel edge-detected its arm against a mirror of core state that a fresh `Nes` had reset, so it never re-armed after a ROM load; the Latency Oracle kept a stale report AND its live Apply button across a ROM change (a depth measured on game A, one click from being applied to game B); and the RAM Atlas would have kept 2,048 labels that look like a map. There is now ONE hook — `DebuggerOverlay::clear_rom_bound_analysis`, called from all three ROM-transition sites in `app.rs` beside `clear_tas_editor`. Add new ROM-bound panels to it rather than adding a fourth per-panel clear. + - **The libretro wrapper is where the bugs live, not the core.** A v2.3.5 audit found five defects and every one was a *wrapper* defect with correct emulation behind it: hardcoded 60.0988 fps for every cartridge (**PAL ran 20.2% fast**), `retro_get_region` unimplemented, `retro_reset` unimplemented (**RetroArch's Reset did nothing, ever** — the library default is a literal no-op), `retro_unload_game` unimplemented (Game Genie indices leaked across cartridges), `aspect_ratio = 0.0` (square pixels; the desktop frontend applies 8:7), and no controller info (**the Zapper was unreachable** despite `Nes::set_zapper` being fully implemented). When something looks wrong in RetroArch, suspect what the wrapper *advertises* before suspecting the emulation. Prefer DERIVING declared values from `rustynes_core` constants (`FRAME_DURATION_*`, `DEFAULT_SAMPLE_RATE`) over transcribing them — the 60.0988 literal had lost all connection to the constant it was copied from. - **`cargo test ` that matches nothing prints `0 passed` and exits 0 — that is NOT a pass.** Bit twice in one session: `cargo test --workspace --features test-roms accuracycoin` and `cargo test -p rustynes-cpu nestest` both matched zero tests and looked green. The real invocations are `cargo test -p rustynes-test-harness --features test-roms --test accuracycoin` (prints `pass rate = 100.00% over 141 assigned tests`; **the RAM decoder is authoritative — the framebuffer decoder reports 120 and is known-buggy**) and `--test nestest`. Always confirm a non-zero test count before reporting a gate as green. diff --git a/CHANGELOG.md b/CHANGELOG.md index f55d9fea..d7d76c2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,75 @@ cycle-accurate core later replaced. ## [Unreleased] +## [2.3.6] - 2026-08-17 - "Sounding" (measuring, and what a measurement may claim) + +A *sounding* is a depth measured with its uncertainty attached, and that is what +every workstream here has in common. Two shipped features are found not to work at +all; two new tools are added that decline to answer rather than guess; and an +optimization campaign is closed on the strength of three measured rejections. + +### The release in one line each + +- **Pixel Provenance never worked, in any release since v2.3.2** — and two comments + plus four doc claims asserted the opposite of their own code, which is why nobody + checked. +- **Duck Hunt could never score.** The Zapper light probe was exactly inverted + against the protocol the game uses. +- **The Latency Oracle** measures the game's own input lag instead of leaving it to + a manual frame-advance ritual — and recommends a run-ahead depth without ever + applying one. +- **The RAM Atlas** classifies all 2 KiB of work RAM, then verifies a candidate by + perturbing it — the step that separates causation from coincidence. +- **The Tools and Debug menus** are regrouped by task; Tools had reached twenty flat + entries. +- **APU Workstream D is closed**, on three measured rejections and the mechanism + that explains them. + +### Added + +- **Latency Oracle** (`Tools → Analysis`, spec `docs/latency-oracle.md`). Replays + the current moment twice — once with a probe button held, once with nothing + pressed — and reports the first frame that differs. That index *is* the game's + internal lag, because on a deterministic core two replays of identical state can + differ for exactly one reason. + + It is built to decline rather than guess. `frames` is an `Option`, and `None` and + `Some(0)` are different answers that are never collapsed: `Some(0)` means the game + reacted on the next frame, `None` means the probe could not tell. It probes six + buttons across three observables (framebuffer, then audio, then work RAM) and + requires agreement; `START` is deliberately excluded, because it pauses many games + — a reaction to a menu, not to gameplay, and counting it would over-report. + + **It recommends; it never applies.** Run-ahead is linear in the core's frame cost, + so silently raising it can push a marginal host into dropped frames for a change + the user never asked for. The depth appears with an explicit Apply button, and a + test fails if storing a report ever queues a config write on its own. + +- **RAM Atlas** (`Tools → Analysis`, spec `docs/ram-atlas.md`). Answers what each + byte of work RAM is *for*, in two stages with deliberately different confidence. + Observation classifies every address (untouched / frame tick / rising / falling / + sparse / volatile) and is **correlation only** — `classify` returns all 2048 labels + as `Untested`, so observation is structurally incapable of claiming liveness. + Verification pokes the byte, re-simulates from the same anchor, and compares. + + Liveness is relative to its lens, and every verdict names the one it used: the same + byte is routinely `Live` through work RAM and `Inert` through the framebuffer. + `Untested` is a third state, distinct from `Inert`, because "we did not look" and + "we looked and saw nothing" are different claims. `Inert` is documented as *not* + meaning unused — a byte the game rewrites from a master copy each frame reads inert + because the poke is overwritten. + +- **`rustynes-probe`**, the deterministic re-simulation engine both tools consume: + anchor, replay under controlled variation, locate the first divergence. Trials are + budgeted, and the budget is binding rather than advisory. + +- **`rustynes verify --rom `** attestation tests, closing the last + item of #360. + +- **Docs**: `docs/ram-atlas.md`, `docs/latency-oracle.md`, and + `docs/user-guide/analysis-tools.md`. `docs/pixel-provenance.md` was also added to + the docs-site nav, having been built but unreachable since v2.3.2. + ### Fixed - **Pixel Provenance now works.** The v2.3.2 "Lucid" marquee returned an empty @@ -128,6 +197,116 @@ cycle-accurate core later replaced. accurate and more informative. This is the file RetroArch's core-information screen displays. +- **A probe trial no longer clears or pollutes the caller's rewind ring.** Two + defects at the one site every trial shares. The first is a bug an earlier fix in + this same release reported as closed and did not close: trials restored their + anchor with the loud `Nes::restore`, which clears the rewind ring, and + `latency::measure_in_place` runs up to 21 trials against the live emulator — so + asking how much input lag a game has destroyed the user's rewind history, + twenty-one times over. The earlier fix had changed only the final restore, not the + per-trial one. With the wipe fixed a second defect became visible: the ring then + *grows*, because trial frames are captured like any others, and those frames are + re-simulated and never happened on the user's timeline. Both fixed; the test + asserts the ring returns exactly as it was, since a weaker "not cleared" + assertion is what let the incomplete fix pass review. + + Separately pinned: a trial's samples do not change when the caller has rewind + armed, so **no measurement taken before this fix needed re-running**. The engine's + premise is that a replay from one anchor is bit-identical, so anything that + silently perturbed state would have invalidated the primitive rather than one + measurement. + +- **The audio observable was structurally dead.** `Observable::AudioEnergy` never + saw any audio: the probe's trial loop emptied its buffer and never filled it, so + the energy reduction summed an empty slice and every frame of every trial + reported zero. Nothing failed, because a lens that returns a constant never + disagrees with itself — so the Latency Oracle's audio fallback stage, the one + that exists for a game whose reaction is audible before it is visible, silently + degraded to work RAM, and the RAM Atlas's audio lens would have reported **every + address inert**. The comment above the missing call said "Drain EVERY frame, + whatever the observable" and explained at length why. Found in review on #392. + +- **The RAM Atlas is unavailable during locked sessions.** Both of its actions + advance the live emulator and Verify pokes work RAM, so it is now gated on the + same `writes_locked || hardcore_blocked` predicate `emu.write` uses — netplay, a + TAS record or replay, and RetroAchievements hardcore. Under netplay or a movie it + would diverge a timeline other peers are lockstepped to; under hardcore it is the + memory write that mode exists to forbid. The disabled state names which reason + applies. + +- **An un-perturbable address is reported `Untested`, not `Inert`.** `verify_liveness` + skipped the poke for an address outside work RAM and then let the two identical + trials agree, producing a confident verdict for a byte it never touched — the + exact failure mode that module documents itself as never producing. It is public + and takes a full `u16`, so a CPU-space mirror such as `$0810` is a plausible + caller input. + +- **The Latency Oracle's felt-latency figure is derived, not transcribed.** It + multiplied by a hardcoded NTSC 16.639 ms, understating PAL and Dendy by 20.2% — + the identical figure and mechanism as the v2.3.5 libretro defect, where a + hardcoded 60.0988 fps had lost all connection to the constant it was copied from. + It now uses the console's own `frame_duration`, captured at measurement time. + +### Changed + +- **Upstream libretro `.info` syncs are batched to MINOR releases.** RetroArch + reads its copy from `libretro/libretro-super`, which nothing syncs + automatically; the next sync is v2.4.0, so that copy reads `v2.3.5` through this + line by decision rather than oversight. A stale `display_version` misreports a + number — the v2.2.9 incident was a stale **licence**, misreporting the terms of + distribution — so licence, supported extensions, and declared-capability changes + still sync immediately. Recorded in `docs/libretro/UPSTREAM_SYNC.md`. + +- **The Tools and Debug menus are regrouped by task.** Tools had reached twenty flat + entries spanning cheats, TAS authoring, media capture, multiplayer, ROM inspection + and provenance analysis; Debug listed "CPU" and "Lua Script" as peers in a + fifteen-item column. Tools becomes Cheats at the top level, then Movies & + Recording, Audio, Input, Game Data, Analysis and HD Pack, with Netplay and + RetroAchievements below a separator — they change what the *session* is rather + than being tools pointed at the game. Debug splits into Chip State, Memory and + Execution, plus Symbols. Emulation's two FDS entries become one Famicom Disk + System submenu. + + No entry is removed, none changes what it dispatches, and nothing moves between + top-level menus — only the depth at which it sits. The movie transport's gating + changes shape but not effect: the ROM/netplay condition moves from deciding + whether the submenu can *open* to per-item enabling, so the reachable set is + identical but the user can see which entries are unavailable instead of facing one + opaque disabled label. `docs/user-guide/menus.md` is corrected, and was already + stale before this release touched it — it listed five Tools entries against an + actual twenty, and still documented a "Show Debugger" toggle removed in v1.7.1. + +- **`MAX_RUN_AHEAD_DEPTH` is shared rather than redeclared.** The Latency Oracle's + clamp was a third independent `3`; that constant exists precisely because two + earlier caps drifted apart (PR #358). + +### Documented + +- **APU Workstream D is closed.** The 18.7%-of-frame-time figure stands — it is a + correct v2.3.1 subsystem attribution, visible only because that pass attributed by + source file, since fat LTO inlines the APU into `cpu_clock`. What is settled is + narrower: the figure is **not recoverable by gating per-cycle bookkeeping**, the + only strategy the workstream ever tried. One adoption (C1, shipped in v2.3.5 at + −3.3% to −4.2%), three measured rejections (D1, D3, D6), one declined on + inspection (D5), and two left unmeasured deliberately (D2, D4). + + The three rejections share one mechanical cause, and it generalises to the two + remaining levers: under `lto = "fat"` with `codegen-units = 1` the guarded code is + already inlined, its repeated loads already merged by common-subexpression + elimination, and the elided branches always-not-taken and so perfectly predicted. + Swapping predictable not-taken branches for an equivalent count of loads plus a + predicate is arithmetically a wash. Stated as the rule worth keeping: *"this work + is inert on almost every cycle" predicts a win only if the work is actually + executed* — and under fat LTO with perfect prediction it largely is not. + + Also recorded: D1's run 1 looked like a textbook win at −3.81% (p = 0.00) on a + shipped default workload and was **entirely an artifact** — the order-bias control, + benching the reference against *itself*, drifted −3.73% on that same workload with + no code change at all, because `ab_check.sh` benchmarks the reference immediately + after a 44.9-second fat-LTO compile across all cores. Three conditions that would + justify reopening the workstream are written down, none a variation on per-cycle + gating. Full numbers in `docs/performance.md`. + ## [2.3.5] - 2026-08-16 - "Manifest" (what the core declares about itself — and the APU, measured at last) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index e0856123..4b5f07f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,7 @@ dependencies = [ [[package]] name = "rustynes-android" -version = "2.3.5" +version = "2.3.6" dependencies = [ "android-activity", "android_logger", @@ -4298,7 +4298,7 @@ dependencies = [ [[package]] name = "rustynes-apu" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4311,7 +4311,7 @@ dependencies = [ [[package]] name = "rustynes-cheevos" -version = "2.3.5" +version = "2.3.6" dependencies = [ "cc", "ureq", @@ -4319,7 +4319,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4336,7 +4336,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4347,7 +4347,7 @@ dependencies = [ [[package]] name = "rustynes-frontend" -version = "2.3.5" +version = "2.3.6" dependencies = [ "anstyle", "arboard", @@ -4406,18 +4406,18 @@ dependencies = [ [[package]] name = "rustynes-gamedb" -version = "2.3.5" +version = "2.3.6" dependencies = [ "rustynes-core", ] [[package]] name = "rustynes-gfx-shaders" -version = "2.3.5" +version = "2.3.6" [[package]] name = "rustynes-hdpack" -version = "2.3.5" +version = "2.3.6" dependencies = [ "lewton", "png", @@ -4428,7 +4428,7 @@ dependencies = [ [[package]] name = "rustynes-ios" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bytemuck", "cpal", @@ -4442,7 +4442,7 @@ dependencies = [ [[package]] name = "rustynes-libretro" -version = "2.3.5" +version = "2.3.6" dependencies = [ "libc", "rust-libretro", @@ -4451,7 +4451,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4463,7 +4463,7 @@ dependencies = [ [[package]] name = "rustynes-mobile" -version = "2.3.5" +version = "2.3.6" dependencies = [ "rustynes-core", "rustynes-hdpack", @@ -4478,7 +4478,7 @@ dependencies = [ [[package]] name = "rustynes-netplay" -version = "2.3.5" +version = "2.3.6" dependencies = [ "futures-util", "js-sys", @@ -4494,7 +4494,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.3.5" +version = "2.3.6" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4506,21 +4506,21 @@ dependencies = [ [[package]] name = "rustynes-probe" -version = "2.3.5" +version = "2.3.6" dependencies = [ "rustynes-core", ] [[package]] name = "rustynes-ra" -version = "2.3.5" +version = "2.3.6" dependencies = [ "rustynes-cheevos", ] [[package]] name = "rustynes-script" -version = "2.3.5" +version = "2.3.6" dependencies = [ "mlua", "piccolo", @@ -4531,7 +4531,7 @@ dependencies = [ [[package]] name = "rustynes-test-harness" -version = "2.3.5" +version = "2.3.6" dependencies = [ "insta", "png", diff --git a/Cargo.toml b/Cargo.toml index e718de09..7cc99395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ default-members = ["crates/rustynes-libretro"] # So: bump this as part of the release cut, together with the CHANGELOG's # `[Unreleased]` -> `[X.Y.Z]` rename and `rustynes_libretro.info`'s # `display_version`, and not before. -version = "2.3.5" +version = "2.3.6" edition = "2024" rust-version = "1.96" license = "GPL-3.0-or-later" diff --git a/README.md b/README.md index 280e485f..561f81af 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Build Status License: GPL-3.0-or-later Version Rust: 1.96
+ Build Status License: GPL-3.0-or-later Version Rust: 1.96
AccuracyCoin nestest Try in browser
Platform

@@ -668,7 +668,9 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **v2.3.5 "Manifest"** — about what the emulator +RustyNES's current release is **v2.3.6 "Sounding"** — about measuring, and +about what a measurement is allowed to claim. Built on **v2.3.5 "Manifest"**, +which was about what the emulator tells the outside world about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms; it does, because RetroArch reads a **separate copy** of the core metadata in `libretro/libretro-super` that the v2.2.9 GPL @@ -685,11 +687,13 @@ implementation did change (the mix specialization is a strict specialization, no no-op), so the accuracy contract was **verified rather than asserted**: AccuracyCoin holds at exactly 141/141, nestest 0-diff. -Two things this release deliberately does **not** claim: RetroArch will still show -the wrong licence until upstream merges the sync, and RustyNES still will not -appear on iOS / iPadOS / tvOS — the cause is now understood (a hardcoded core list -in `libretro/RetroArch`, not a build failure) but the remedy lands in a repository -this project does not control. +Two things v2.3.5 deliberately did **not** claim have since landed upstream +(2026-08-16): `libretro-super#2069` merged, so RetroArch now reads `GPLv3+`, and +`RetroArch#19416` merged, so RustyNES is in the App Store core list. Being in that +list is not the same as being installable — iOS / iPadOS / tvOS availability +arrives with the next App Store RetroArch build, on libretro's cadence. One item +is still open: `libretro/docs#1180`, the licence on the libretro documentation +site. Built on **v2.3.3 "Cadence"** — the display-pacing release, which closed the one measured artefact whose signature matches the reported picture "shudder" without diff --git a/VERSION-PLAN.md b/VERSION-PLAN.md index e2a72679..37544e8e 100644 --- a/VERSION-PLAN.md +++ b/VERSION-PLAN.md @@ -1,6 +1,6 @@ # RustyNES Version Plan -**Current release: v2.3.5 "Manifest"** — the declaration release: what the core says about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms, and it was: RetroArch reads `dist/info/` from **libretro/libretro-super**, a SEPARATE copy nothing synced, so the v2.2.9 GPL relicense never reached the file users see. Corrected to `GPLv3+` with a standing `libretro_info_audit.rs` that makes the upstream sync a **copy** rather than a re-derivation, and a licence change is now a mandatory upstream-sync trigger. Auditing the wrapper then found **five further defects, every one with correct emulation behind it** — PAL ran 20.2% fast, Reset did nothing ever, unload leaked Game Genie indices, the aspect ratio assumed square pixels, and the Zapper was unreachable — plus a **use-after-free** in the controller tables caught in review. The crate went from zero tests to eight. The APU also gained its first throughput bench and a default-configuration mix specialization (−3.3% to −4.2% on `nes_run_frame_nestest`), so **AccuracyCoin 141/141 was VERIFIED, not asserted**. Built on **v2.3.4 "Ledger"** — the coverage release: three boards (mapper 176 submapper 2 WAIXING-FS005, 154 NAMCOT-3453, 243 Sachen SA-020A, breadth **172 → 174 families**), the coverage harness moved onto the frontend's real load path, and the defect that exposed — the per-game database reading a `0` Mapper column as "force NROM" and overwriting correct headers, leaving **every Sachen cartridge** unloadable since **v1.2.0**. **This release touches the emulation core**, so AccuracyCoin exactly 141/141 is **verified, not asserted by construction**. Its Workstream C (the APU at 18.7% of frame time) was carried to v2.3.5 and delivered there. Built on **v2.3.3 "Cadence"** — the display-pacing release: the run-ahead throttle oscillation traced to a stale median (a gate counting 120 frames of a 600-sample ring), a predictive engage arm that converges a `run_ahead = 3` host in 2.8 s instead of 12.1 s, and the `wp_presentation` measurement apparatus that made the diagnosis possible. **No emulation-core changes** (AccuracyCoin exactly 141/141). Built on **v2.3.2 "Lucid"** (pixel provenance + deterministic replay attestation), **v2.3.1 "Plumb Line"** (ten measured rejections), and **v2.3.0 "Datum II"**, the capstone that **closed** the v2.2.6 → v2.3.0 line (true multi-viewport OS-window detach, the emulator-lock frame-pacing fix, a −5.1% byte-identical PPU optimization, and both forum-reported accuracy items verified already-correct) — all on the **v2.0.0 "Timebase"** MAJOR base (the one-clock / every-cycle-bus-access scheduler rewrite). **v1.0.0** was the first stable, production cut. As of **v2.2.9**, RustyNES is **GPL-3.0-or-later** — a derivative work of GPL-licensed emulators (ADR 0036); a licensing correction, **not** a SemVer break (no public-API or save-state change). `docs/STATUS.md` is the authoritative current-state record; `CHANGELOG.md` carries the full per-release history. +**Current release: v2.3.6 "Sounding"** — measuring, and what a measurement may claim. Two shipped features are found never to have worked: **Pixel Provenance** returned an empty report for every user on the default `run_ahead = 1` (its rollback is the last thing before the frontend takes the lock, so the panel always looked after the wipe) and "click any pixel" was never implemented — two comments and four doc claims asserted the opposite of their own code, which is why four releases passed unchecked; and **Duck Hunt could never score**, its Zapper probe exactly inverting the "see nothing, then a bright spot" protocol. Two new tools built to **decline rather than guess**: the **Latency Oracle** (measures the game's own input lag; recommends a run-ahead depth and never applies one) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then *verifies* a candidate by perturbing it — `Untested` is a third state distinct from `Inert`, and liveness names its lens). **APU Workstream D is closed** on three measured rejections plus the fat-LTO mechanism explaining them. Tools and Debug are regrouped by task. Core gains one `const fn` getter, so AccuracyCoin 141/141 is verified, not asserted. Built on **v2.3.5 "Manifest"** — the declaration release: what the core says about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms, and it was: RetroArch reads `dist/info/` from **libretro/libretro-super**, a SEPARATE copy nothing synced, so the v2.2.9 GPL relicense never reached the file users see. Corrected to `GPLv3+` with a standing `libretro_info_audit.rs` that makes the upstream sync a **copy** rather than a re-derivation, and a licence change is now a mandatory upstream-sync trigger. Auditing the wrapper then found **five further defects, every one with correct emulation behind it** — PAL ran 20.2% fast, Reset did nothing ever, unload leaked Game Genie indices, the aspect ratio assumed square pixels, and the Zapper was unreachable — plus a **use-after-free** in the controller tables caught in review. The crate went from zero tests to eight. The APU also gained its first throughput bench and a default-configuration mix specialization (−3.3% to −4.2% on `nes_run_frame_nestest`), so **AccuracyCoin 141/141 was VERIFIED, not asserted**. Built on **v2.3.4 "Ledger"** — the coverage release: three boards (mapper 176 submapper 2 WAIXING-FS005, 154 NAMCOT-3453, 243 Sachen SA-020A, breadth **172 → 174 families**), the coverage harness moved onto the frontend's real load path, and the defect that exposed — the per-game database reading a `0` Mapper column as "force NROM" and overwriting correct headers, leaving **every Sachen cartridge** unloadable since **v1.2.0**. **This release touches the emulation core**, so AccuracyCoin exactly 141/141 is **verified, not asserted by construction**. Its Workstream C (the APU at 18.7% of frame time) was carried to v2.3.5 and delivered there. Built on **v2.3.3 "Cadence"** — the display-pacing release: the run-ahead throttle oscillation traced to a stale median (a gate counting 120 frames of a 600-sample ring), a predictive engage arm that converges a `run_ahead = 3` host in 2.8 s instead of 12.1 s, and the `wp_presentation` measurement apparatus that made the diagnosis possible. **No emulation-core changes** (AccuracyCoin exactly 141/141). Built on **v2.3.2 "Lucid"** (pixel provenance + deterministic replay attestation), **v2.3.1 "Plumb Line"** (ten measured rejections), and **v2.3.0 "Datum II"**, the capstone that **closed** the v2.2.6 → v2.3.0 line (true multi-viewport OS-window detach, the emulator-lock frame-pacing fix, a −5.1% byte-identical PPU optimization, and both forum-reported accuracy items verified already-correct) — all on the **v2.0.0 "Timebase"** MAJOR base (the one-clock / every-cycle-bus-access scheduler rewrite). **v1.0.0** was the first stable, production cut. As of **v2.2.9**, RustyNES is **GPL-3.0-or-later** — a derivative work of GPL-licensed emulators (ADR 0036); a licensing correction, **not** a SemVer break (no public-API or save-state change). `docs/STATUS.md` is the authoritative current-state record; `CHANGELOG.md` carries the full per-release history. RustyNES follows [Semantic Versioning 2.0.0](https://semver.org/). diff --git a/crates/rustynes-libretro/rustynes_libretro.info b/crates/rustynes-libretro/rustynes_libretro.info index 9b08069f..400d5d63 100644 --- a/crates/rustynes-libretro/rustynes_libretro.info +++ b/crates/rustynes-libretro/rustynes_libretro.info @@ -5,7 +5,7 @@ supported_extensions = "nes|fds" corename = "RustyNES" license = "GPLv3+" permissions = "" -display_version = "v2.3.5" +display_version = "v2.3.6" categories = "Emulator" # Hardware Information diff --git a/docs/STATUS.md b/docs/STATUS.md index a5150fd4..b7231b93 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,37 @@ # RustyNES — Project Status Matrix -> **Current release: v2.3.5** (2026-08-16) — **"Manifest"**, about what the core +> **Current release: v2.3.6** (2026-08-17) — **"Sounding"**, about measuring and +> about what a measurement is allowed to claim. **Two shipped features are found +> never to have worked.** Pixel Provenance, the v2.3.2 marquee, returned an empty +> report for every user on the default `run_ahead = 1` — its per-frame rollback is +> the last thing before the frontend releases the emulator lock, so the panel's +> first look was always *after* the wipe — and "click any pixel" was never +> implemented at all. Two source comments and four doc claims asserted the +> opposite of their own code, which is why four releases passed unchecked. And +> **Duck Hunt could never score**: the Zapper light probe was sampled at +> end-of-frame, exactly inverting the "see nothing, then a bright spot" protocol +> the game depends on (000000 → 000500). +> +> Two new tools, both built to **decline rather than guess**. The **Latency +> Oracle** measures the game's own input lag by replaying one moment with a button +> held and without it; `None` and `Some(0)` are different answers and are never +> collapsed, `START` is excluded from the probe buttons because pausing is a +> reaction to a menu rather than to gameplay, and it **recommends a run-ahead depth +> without ever applying one**. The **RAM Atlas** classifies all 2 KiB of work RAM +> and then *verifies* a candidate by perturbing it — the step that separates +> causation from coincidence. Observation returns all 2048 labels as `Untested`, so +> it is structurally incapable of claiming an effect; liveness is relative to its +> lens and every verdict names it; and `Inert` is documented as **not** meaning +> unused. +> +> **APU Workstream D is closed.** The 18.7%-of-frame figure stands; what is settled +> is that it is not recoverable by gating per-cycle bookkeeping — one adoption, +> three measured rejections, one declined on inspection, two left unmeasured +> deliberately, and the fat-LTO mechanism that explains all three nulls. The Tools +> and Debug menus are regrouped by task (Tools had twenty flat entries). **The core +> gains one `const fn` getter and nothing else, so AccuracyCoin exactly 141/141 and +> nestest 0-diff are verified, not asserted.** Built on **v2.3.5** (2026-08-16) — +> **"Manifest"**, about what the core > *declares* about itself. The libretro metadata RetroArch reads is a **separate > copy** in `libretro/libretro-super` that the v2.2.9 GPL relicense never reached, > so the frontend still advertises MIT/Apache-2.0 at v2.2.1; the repo-side half is @@ -24,10 +55,14 @@ > than asserted**: AccuracyCoin **141/141** (RAM decoder authoritative), nestest > 0-diff. > -> **Not fixed by this release:** RetroArch still shows the wrong licence until -> `libretro/libretro-super` and `libretro/docs` merge the sync, and RustyNES still -> does not appear on iOS / iPadOS / tvOS — a hardcoded `appstore_cores` list in -> `libretro/RetroArch`, not a build failure. Both land upstream. +> **Not fixed by v2.3.5, and since RESOLVED upstream (2026-08-16):** +> `libretro-super#2069` merged, so RetroArch now reads `GPLv3+`; and +> `RetroArch#19416` merged, so `rustynes` is in the `appstore_cores` list — +> verified against that repository's `master`, not the PR state. Being in the +> build list is not the same as being installable: iOS / iPadOS / tvOS +> availability arrives with the next App Store RetroArch build, on libretro's +> cadence. **`libretro/docs#1180` remains open** — the licence on the libretro +> documentation site. > > **Previous release: v2.3.4** (2026-08-15) — **"Ledger"**, the coverage release. > Three boards land — **mapper 176 submapper 2** (WAIXING-FS005), **154** diff --git a/docs/libretro/UPSTREAM_SYNC.md b/docs/libretro/UPSTREAM_SYNC.md index 98949cb1..4a7c6911 100644 --- a/docs/libretro/UPSTREAM_SYNC.md +++ b/docs/libretro/UPSTREAM_SYNC.md @@ -50,6 +50,8 @@ Given that RustyNES's license is itself the outcome of a corrected provenance fa **Therefore:** 1. **A license change is a mandatory upstream-sync trigger**, on the same footing as a release. It is not a documentation-only change. + + **A version bump alone is not.** Maintainer decision at the v2.3.6 cut: upstream syncs are batched to MINOR releases, so the next one is **v2.4.0**. The upstream `dist/info/rustynes_libretro.info` therefore reads `display_version = "v2.3.5"` through the v2.3.6-v2.3.9 line, deliberately and not by oversight. The distinction that makes this safe is the one this whole document exists for: a stale `display_version` misreports a number, whereas a stale `license` misreports the terms under which the software is distributed — which is what actually went wrong in v2.2.9. Anything touching `license`, `supported_extensions`, or the core's declared capabilities still syncs immediately, regardless of where the version line sits. 2. `crates/rustynes-test-harness/tests/libretro_info_audit.rs` now pins the local `.info` against **two different sources of truth**, one per field, so the local file cannot drift and the upstream sync is a **copy**, never a re-derivation: - `license` and `display_version` — against `[workspace.package]` in the root `Cargo.toml`. - `supported_extensions` — against the **core's own** `retro_get_system_info` declaration in `crates/rustynes-libretro/src/lib.rs`, not the manifest, because that is where the list the core will actually load is defined. A literal repeated in the test would be a second copy of the fact rather than an audit of it.