From 1a0794c0e7766ecd5eafe37c76922b7ac9e98921 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Tue, 21 Jul 2026 02:59:00 -0400 Subject: [PATCH 1/2] chore(release): v2.2.2 "Conduit" Cuts v2.2.2, a build, distribution, and CI-integrity patch. It carries RustyNES onto RetroArch's own buildbot -- the recipe now builds all ten platform jobs -- hardens the GitHub Actions supply chain, and collapses the toolchain to a single pinned source of truth with no `nightly` on any build path. Zero emulation-core changes --------------------------- No file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched BY CONSTRUCTION rather than by re-measurement: AccuracyCoin holds 141/141 (100.00%), nestest stays 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle are unchanged from v2.2.1. One behavioural improvement reaches a shipped artifact: the libretro tvOS core is now built with `panic = "abort"` like every other platform, rather than the `panic = "unwind"` its former `-Zbuild-std` path forced. Scope (all landed since v2.2.1) ------------------------------- * Libretro buildbot recipe (issue #311) + core feature completion: memory maps for rcheevos, an FDS load-path fix plus disk control, native Game Genie cheats, fast-forward audio gating. * Buildbot taken from 1/10 to 10/10 across three diagnosis rounds against a third-party pipeline we can neither push to nor re-run: missing cross targets (8 jobs), an upstream `rust-libretro` MinGW enum-signedness bug, tvOS MSRV + `panic_abort`, and `-C ar` removed in Rust 1.97. * tvOS moved onto the pinned stable toolchain: `aarch64-apple-tvos` is no longer tier 3, so the template's `+nightly -Zbuild-std` override is obsolete. Dropping it dissolved three stacked workarounds instead of adding a fourth. * CI supply-chain hardening: `persist-credentials: false` on all 19 checkouts, a fail-closed release-tag check via `git/matching-refs`, and `dtolnay/rust-toolchain` SHA-pinned off `@master`. * One toolchain everywhere: `rust-setup` resolves `[toolchain].channel` from `rust-toolchain.toml`, leaving zero toolchain version literals under `.github/`. Nightly survives only in `cargo fuzz` and the dormant `rustynes-monetization` bindgen, neither of which is a gate. * Dependency consolidation (Dependabot #313-#315), which surfaced a real lz4_flex 0.14 `alloc`-feature break that only the standalone `no_std` cross-compile can catch. Version and metadata -------------------- Workspace 2.2.1 -> 2.2.2; `Cargo.lock` diff is exactly 18 insertions and 18 deletions, all workspace-member version lines, with no dependency drift. No mobile app version change -- Android `versionCode`/`versionName` and iOS `MARKETING_VERSION` stay frozen at their v2.0.x host-only release points; the joint mobile store launch remains v2.3.0. README badge, README Current Release, `docs/STATUS.md`, and AGENTS.md current-release blocks all updated. The RetroAchievements User-Agent needs no edit: it derives from `CARGO_PKG_VERSION`. `.gitignore` gains `*.dSYM/` and `*.dSYM.zip`. The Apple libretro jobs and the iOS xcframework build emit debug-symbol bundles beside the library; `*.dylib` matched the library but neither the bundle directory nor the archive, so a local Apple libretro build left both in the working tree. Release notes ------------- `.github/release-notes/v2.2.2.md` is the maintainer-authored override that `release-auto.yml` prefers over the CHANGELOG section. Verification ------------ cargo test --workspace: 1905 passed, 0 failed, 13 ignored (exit 0). fmt clean; clippy --workspace --all-targets -D warnings clean; rustdoc clean under RUSTDOCFLAGS=-D warnings; `no_std` thumbv7em cross-compile builds; `cargo deny check` ok on advisories, bans, licenses and sources; pre-commit (markdownlint included) clean on every touched file. Not verifiable on this host: the tvOS link step. There is no tvOS SDK or linker here and we cannot run a pipeline on git.libretro.com, so the compile path is verified and the link step is not. Issue #311 stays open until libretro confirms a clean pipeline. --- .github/release-notes/v2.2.2.md | 93 +++++++++++++++++++++++++++++++++ .gitignore | 8 +++ AGENTS.md | 8 +-- CHANGELOG.md | 21 ++++++++ Cargo.lock | 36 ++++++------- Cargo.toml | 2 +- README.md | 17 +++++- docs/STATUS.md | 19 ++++++- 8 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 .github/release-notes/v2.2.2.md diff --git a/.github/release-notes/v2.2.2.md b/.github/release-notes/v2.2.2.md new file mode 100644 index 00000000..69194140 --- /dev/null +++ b/.github/release-notes/v2.2.2.md @@ -0,0 +1,93 @@ +# RustyNES v2.2.2 — "Conduit" (libretro buildbot 10/10 + CI supply-chain hardening + single-source toolchain) + +A **build, distribution, and CI-integrity patch**. It carries RustyNES onto RetroArch's own buildbot — the recipe now builds **all ten platform jobs**, after three rounds of diagnosis against a third-party pipeline we can neither push to nor re-run — hardens the GitHub Actions supply chain, and collapses the toolchain to a single pinned source of truth with **no `nightly` on any build path**. + +**Zero emulation-core changes.** No file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched *by construction*: **AccuracyCoin holds 141/141 (100.00%)**, nestest stays 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle are all unchanged from v2.2.1. + +One behavioural improvement does reach a shipped artifact: the libretro **tvOS** core is now built with `panic = "abort"` like every other platform, instead of the `panic = "unwind"` its previous `-Zbuild-std` path forced. + +## Added + +### Libretro buildbot recipe — the last step into RetroArch's core downloader + +`.gitlab-ci.yml` covering Windows x64, Linux x64, macOS x64/arm64, Android (4 ABIs), iOS arm64, and tvOS arm64 via the shared `libretro-infrastructure/ci-templates`. Two structural fixes were needed to make the centrally-maintained templates work against this workspace, neither of which the templates can be asked to accommodate per-core: + +- **Artifact-name collision.** The templates always run a fixed `mv lib${CORENAME}. ${CORENAME}_libretro.`, which would have doubled the `_libretro` suffix because the crate's compiled library name already contained it. `crates/rustynes-libretro/Cargo.toml` now sets `[lib] name = "rustynes"` (its `[package] name` stays `rustynes-libretro` so `-p rustynes-libretro` scoping is unaffected everywhere else), making `CORENAME=rustynes` resolve both halves correctly to the already-published `rustynes_libretro.info` name. +- **Build scoping.** The templates invoke a bare `cargo build --release --target ` from the repo root with no `-p` flag. `[workspace] default-members = ["crates/rustynes-libretro"]` scopes that to the one crate instead of all 18 workspace members — in particular keeping the frontend's wgpu / winit / cpal / Wayland stack out of a cross-compile that has no business linking it. + +### Libretro core feature completion + +Native `RETRO_ENVIRONMENT_SET_MEMORY_MAPS` registration — the memory-descriptor path `rcheevos` prefers, alongside the existing legacy pointer API, with VRAM deliberately registered under a named `"PPU"` address space rather than the default CPU space (on the CPU bus `$2000-$2007` are PPU MMIO registers, not video RAM). An **FDS load-path fix**: `.fds` content was previously routed through the cartridge-only path and failed silently despite `valid_extensions` advertising it; it now detects the extension, routes to `Nes::from_disk`, and locates `disksys.rom` via `GET_SYSTEM_DIRECTORY` — plus a full disk-control interface for multi-side swapping from RetroArch's Quick Menu. Native Game Genie cheats (`on_cheat_set` / `on_cheat_reset`, deliberately excluded from serialized state). A `get_fastforwarding`-gated audio-push skip during RetroArch's fast-forward / rollback catch-up. + +TAS and netplay needed **no** new libretro-side wiring: RetroArch's own rollback and movie/rewind machinery already ride the existing `on_serialize` / `on_unserialize` hooks. + +### `libretro-cross` CI guard job + +`.github/workflows/ci.yml` gains a job cross-checking `rustynes-libretro` against the buildbot ABI families a Linux runner can model faithfully — MinGW-Windows and Android/NDK. There was previously **zero** libretro coverage in GitHub Actions, which is precisely why every defect below reached a third-party buildbot first. The Apple families are excluded deliberately: bindgen needs a real per-target sysroot, there is no Apple SDK on a Linux runner, and feeding it host glibc headers would generate Apple bindings from Linux headers — a lookalike, not a rehearsal. + +## Fixed + +### Buildbot pipeline: 1 of 10 jobs green → all 10 building + +The first run ([pipeline #91899](https://git.libretro.com/libretro/RustyNES/-/pipelines/91899)) passed only `libretro-build-linux-x64`. Four independent defects surfaced across three rounds — all on our side, none in libretro's build images: + +- **Missing cross-compile targets (8 jobs).** `rust-toolchain.toml` pins the channel, so rustup installs a *fresh* toolchain inside the build image carrying only the host std plus the two targets that file declares — bypassing the image's own default toolchain, on which every libretro cross target is pre-provisioned. Every non-host job died with `E0463: can't find crate for core`; Linux x64 survived only because its target *is* the host triple. Each job now runs `rustup target add ${RUST_TARGET}` into the pinned toolchain. Deliberately **not** solved by extending `rust-toolchain.toml`'s `targets = [...]`, which would make every contributor and every GitHub Actions job download ~8 extra `rust-std` components on each toolchain install — a large permanent cost on everyone to serve one downstream CI. +- **Upstream `rust-libretro` MinGW ABI bug (Windows).** Masked behind the target failure and never previously reached. `rust-libretro 0.3.2` casts a keycode to `i32` under `cfg(target_family = "windows")`, but C enum signedness follows the **ABI**, not the OS family: only the MSVC ABI gives plain enums `int`. Under MinGW — what the buildbot's `x86_64-pc-windows-gnu` job uses — bindgen emits `retro_key(c_uint)` and the crate fails with `E0308`. Upstream has had no commit since 2023-02 and 0.3.2 is its newest release, so `.cargo/config.toml` points bindgen's clang at the matching MSVC triple for **both** MinGW targets (`x86_64-pc-windows-gnu` for the buildbot, `i686-pc-windows-gnu` for the crate `Makefile`'s legacy libretro-super path, verified to fail identically). Surgical: the generated bindings differ by 28 lines, all enum signedness, with no struct layout, signature, or type size affected. +- **tvOS MSRV and `panic_abort`.** Its template overrides `script` with `cargo +nightly build -Zbuild-std`, which bypasses the channel pin onto the image's stale nightly — below the workspace MSRV, failing cargo's gate before compiling anything. +- **tvOS `-C ar`, removed in Rust 1.97.** Refreshing that nightly then hit ``error: `-C ar`: this option has been removed``. The build image injects `-Car=,Clink-arg=...` into *every* Apple job, and that long-deprecated no-op became a hard error in 1.97 (bisected: 1.96.1 warns, 1.97.1 errors). + +The last two are now moot rather than worked around — see below. + +### tvOS builds on the pinned stable toolchain + +`aarch64-apple-tvos` was a **tier-3** target when the upstream template was written, with no distributed `rust-std`, making `-Zbuild-std` — and therefore nightly — the only way to build it. **The target has since been promoted and rustup now ships a complete prebuilt std for it, `panic_abort` included.** Verified against the pinned toolchain: `rustup target add aarch64-apple-tvos` installs 26 rlibs, and `cargo check --release -p rustynes-libretro --target aarch64-apple-tvos` compiles the entire dependency graph, bindgen included. + +The job now restores the shared Apple build script via `!reference [.libretro-rust-apple-base, script]` rather than duplicating it, so upstream fixes keep reaching it. That single change **dissolved three stacked workarounds instead of adding a fourth**: the nightly-channel reinstall, the `CARGO_PROFILE_RELEASE_PANIC=unwind` override, and the `-C ar` clearing. tvOS is now identical in shape to the osx / ios jobs and honours `panic = "abort"` like every other platform. + +`-C ar` remains a latent hazard for **all four** Apple jobs — they are green only because the pinned channel treats it as a warning — so the hazard and its remedy are recorded in `rust-toolchain.toml`, at the line a channel bump would edit. + +## Security + +### `persist-credentials: false` on all 19 CI checkouts + +`actions/checkout` defaults to writing the workflow `GITHUB_TOKEN` into `.git/config`, where any code the job subsequently executes from the checkout — Cargo build scripts, proc macros, test binaries, Gradle build scripts, shell scripts, the MkDocs build — can read it. On a pull request that tree is by definition unreviewed code, and nearly every job compiles or runs it. + +Audited rather than applied blanket: `.github/actions/rust-setup` performs no checkout of its own, so call-site hardening is complete coverage; and **no job was found to need the credential** — nothing pushes commits, tags, or branches, there are no submodules, GitHub Pages uses the OIDC flow rather than a `gh-pages` push, and `gh release create` / `softprops/action-gh-release` / `fastlane match` authenticate by other means. The highest-exposure job is `web.yml`'s `build`, which declares `pages: write` + `id-token: write` at *workflow* level. + +### Fail-closed release-tag check + +`release-auto.yml`'s tag-existence check previously ran `git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1`, collapsing three distinct outcomes — tag present, tag absent, and *lookup failed* — into two, with any non-zero exit read as "absent". A transient network or auth blip would therefore have pushed an already-released version down the release path. + +It now queries `gh api .../git/matching-refs/tags/`, chosen over `git/ref/tags/` because it answers "absent" with HTTP 200 and an empty array instead of a 404, so a genuine miss can never be mistaken for an error. `matching-refs` matches by **prefix**, so the exact ref is compared in `jq` — load-bearing, not defensive: `v2.2` prefix-matches two real tags while exact-matching none. Under `set -euo pipefail` a `gh` failure and a non-array body both abort the job. + +### `dtolnay/rust-toolchain` SHA-pinned + +Moved off `@master` — a branch that advances on every upstream commit, so each run silently resolved to whatever HEAD happened to be. That composite feeds 12 of the repo's 19 checkouts, including `release.yml` (`contents: write`, builds the shipped binaries) and `web.yml` (`pages: write` + `id-token: write`), and it is the action that *installs the compiler* — the hardest place to notice a compromise from build output. The trailing `# v1` comment keeps Dependabot's `github-actions` ecosystem able to advance the pin. + +### Fuzz and parser hardening carried forward + +The three whitespace-fixing pre-commit hooks gained a shared `exclude` anchor so `pre-commit run --all-files` can no longer rewrite vendored and upstream trees (it had silently reformatted 41 files across the vendored TriCNES C#, vendored rcheevos C, `ref-docs/`, an upstream font licence, and upstream test-ROM READMEs — destroying exactly the byte-identical-to-upstream property those trees exist for). Scope is deliberately narrower than `.markdownlintignore`: only content we did not author. + +## Changed + +### One toolchain, resolved from one file + +`.github/actions/rust-setup` now parses `[toolchain].channel` out of `rust-toolchain.toml` and installs that, so **no toolchain version literal remains anywhere under `.github/`** and a toolchain bump is a one-line edit. + +Previously the composite defaulted to `stable` and 5 of its 12 call sites overrode that explicitly. This was **misleading rather than broken**: `rust-toolchain.toml` is a directory override that outranks the `rustup default` the action performs, so every job was *already* compiling on the pinned channel (rustup logs `overridden by .../rust-toolchain.toml`). The `stable` default merely installed a second toolchain nothing used — and made the workflows *read* as though the matrix tested against latest stable, a guarantee the repo appeared to offer and never provided. + +Extraction is scoped to the `[toolchain]` table rather than "the first `channel` key in the file": a `channel` key in any earlier table would otherwise be installed instead, which for a `channel = "nightly"` decoy would have silently reintroduced the toolchain this release removes while still reporting success. Anything unparseable — missing table, single-quoted value, empty file — aborts the job with a `::error::` annotation instead of guessing. + +**Nightly now survives in exactly two places, neither of them a build, test, lint, docs, release, or packaging gate:** `cargo fuzz`, which requires it for the sanitizer flags it threads through `rustc`, and the dormant `rustynes-monetization` crate's standalone `uniffi-bindgen`. Neither can move to stable without breaking the tool, so both are documented rather than quietly worked around. + +### Dependency consolidation (closes Dependabot #313–#315) + +All three open Dependabot PRs rolled into one reviewed change plus a full `cargo update --workspace` sweep, with **no source changes** and the `#![no_std]` core untouched. This surfaced a real latent break: **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`), having split no_std support into an `alloc`-vs-`std` distinction that did not exist in 0.13. A `cargo build --workspace` cannot catch this — `rustynes-core`'s default-on `std` feature implies it through cargo's feature unification — only the standalone `no_std` cross-compile does. + +## Notes + +- Version: workspace `2.2.1 → 2.2.2`. No mobile app version change (`versionCode` / `versionName` on Android and `MARKETING_VERSION` on iOS stay frozen at their v2.0.x host-only release points; the joint mobile store launch remains **v2.3.0**). +- `.gitignore` gains `*.dSYM/` and `*.dSYM.zip` — the Apple libretro jobs and the iOS xcframework build emit debug-symbol bundles beside the library, which `*.dylib` did not cover. +- **No accuracy or oracle number moves.** AccuracyCoin 141/141, nestest 0-diff, `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle all unchanged. Save-state / TAS / netplay-replay formats untouched. +- Full host gate green: fmt · clippy (all feature combos + wasm32) · rustdoc (`-D warnings`) · `no_std` cross-compile (`thumbv7em`) · markdownlint · `cargo deny check` · both `libretro-cross` guard legs. +- **Not verifiable here:** the tvOS *link* step. There is no tvOS SDK or linker on the development host and we cannot run a pipeline on `git.libretro.com`, so the compile path is verified and the link step is not. Issue #311 stays open until libretro confirms a clean pipeline. diff --git a/.gitignore b/.gitignore index 48b627b1..7d86d824 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,14 @@ Cargo.lock *.a *.lib *.pdb +# Apple debug symbols. The libretro Apple jobs (osx / ios / tvos) and the iOS +# xcframework build emit `.dylib.dSYM` bundles next to the library, and +# the buildbot additionally zips them for upload — `*.dylib` above covers the +# library but neither the bundle directory nor the archive. Building any Apple +# libretro target locally (`crates/rustynes-libretro/Makefile`) leaves both in +# the working tree. +*.dSYM/ +*.dSYM.zip # --- WebAssembly / trunk / wasm-pack --- /pkg/ diff --git a/AGENTS.md b/AGENTS.md index c462d016..32083929 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,9 @@ 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.2.1** (2026-07-15) — a **housekeeping patch** on top of v2.2.0 "Capstone" (next paragraph): archives two batches of dev/research tooling (the Game Genie header-robust re-key's six research/verification scripts in `scripts/gg/`, and the 2A03-revision DMA-divergence probe in `scripts/probes/`), consolidates six open Dependabot PRs with **zero source changes** (`pollster` 0.4→1.0, `wide` 0.7→1.5, `tungstenite`/`tokio-tungstenite` 0.29→0.30, `bytemuck`/`cc` patch, `actions/setup-python` v5→v6), and wires four gitignored, `RUSTYNES_FDS_BIOS`-gated smoke tests against `TakuikaNinja`'s FDS `$4023` / mirroring / audio-register / DRAM-watchdog hardware-verification probes (regression insurance for behavior RustyNES already models correctly, not a fix — the `$4030.D1` DRAM-refresh-watchdog probe tracks a known, honest residual RustyNES does not model, per `docs/accuracy-ledger.md`). **Zero accuracy, feature, or core changes** — the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched; **AccuracyCoin holds 141/141 (100.00%)**, unchanged from v2.2.0. +**Current release: v2.2.2 "Conduit"** (2026-07-21) — a **build, distribution, and CI-integrity patch**. It takes the **libretro buildbot recipe from 1 of 10 jobs green to all ten building** (three rounds of diagnosis against a third-party pipeline we cannot push to or re-run — the last step before RustyNES lands in RetroArch's built-in core downloader), **hardens the GitHub Actions supply chain** (`persist-credentials: false` on all 19 checkouts, a fail-closed release-tag check via `git/matching-refs`, `dtolnay/rust-toolchain` SHA-pinned off `@master`), and **collapses the toolchain to one pinned source of truth** — `.github/actions/rust-setup` resolves `[toolchain].channel` from `rust-toolchain.toml`, so there is **no toolchain version literal anywhere under `.github/`** and **no `nightly` on any build path** (it survives only in `cargo fuzz` and the dormant `rustynes-monetization` bindgen, neither a gate). **Zero emulation-core changes** — nothing under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched by construction: **AccuracyCoin holds 141/141 (100.00%)**, nestest 0-diff, `pal_apu_tests` 10/10, all unchanged since v2.2.0. One behavioural improvement reaches a shipped artifact: the libretro **tvOS** core is now built with `panic = "abort"` like every other platform, rather than the `panic = "unwind"` its previous `-Zbuild-std` path forced (`aarch64-apple-tvos` is no longer tier 3 — rustup ships a prebuilt std including `panic_abort`, making the upstream template's `+nightly -Zbuild-std` override obsolete). + +The prior release, **v2.2.1** (2026-07-15), was a **housekeeping patch** on top of v2.2.0 "Capstone" (next paragraph): archives two batches of dev/research tooling (the Game Genie header-robust re-key's six research/verification scripts in `scripts/gg/`, and the 2A03-revision DMA-divergence probe in `scripts/probes/`), consolidates six open Dependabot PRs with **zero source changes** (`pollster` 0.4→1.0, `wide` 0.7→1.5, `tungstenite`/`tokio-tungstenite` 0.29→0.30, `bytemuck`/`cc` patch, `actions/setup-python` v5→v6), and wires four gitignored, `RUSTYNES_FDS_BIOS`-gated smoke tests against `TakuikaNinja`'s FDS `$4023` / mirroring / audio-register / DRAM-watchdog hardware-verification probes (regression insurance for behavior RustyNES already models correctly, not a fix — the `$4030.D1` DRAM-refresh-watchdog probe tracks a known, honest residual RustyNES does not model, per `docs/accuracy-ledger.md`). **Zero accuracy, feature, or core changes** — the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched; **AccuracyCoin holds 141/141 (100.00%)**, unchanged from v2.2.0. The prior release, **v2.2.0 "Capstone"** (2026-07-12), was the **milestone cut** that **closes the v2.1.5 → v2.2.0 "deepen the existing project"** run, landing its two remaining marquees — the **netplay matchmaking / lobby** stack and the **FDS medium model** — atop a peripherals + quality/security pass, all sitting on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. Every v2.2.0 change is **additive or default-off**, so the deterministic core is untouched on the shipped default — **AccuracyCoin holds 141/141 (100.00%)**, nestest is 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle / the `#![no_std]` chip stack are all unchanged, and save-state / TAS / netplay replay stay bit-identical. It lands (PRs #290, #291; cut #297): a **netplay lobby + matchmaking** stack over the existing room-code / TURN transport (`ListRooms`/`RoomList` browse-and-join room directory + a server-side `QuickMatch`/`Matched` quick-play path, both **signaling only** with the `room-list` frame parsed by a brace-depth walk bounded at `MAX_ROOM_LIST` = 256); **delayed-stream spectators** (`SpectatorConfig.delay_frames`, clamped to `MAX_DELAY_FRAMES` = 512 — a pure presentation hold that cannot perturb the match); a **hardened desync + peer-liveness surface** (a graded `DesyncStatus` with a hysteresis threshold of 3 ≈ 1.5 s + a graded `PeerLink` / terminal `DisconnectReason::PeerTimeout` on multi-second 2 s / 5 s RTT timeouts — telemetry only, the rollback/determinism contract untouched); the **FDS medium model completion** (F4.3, a marquee — a per-block **CRC-16/KERMIT** re-emitted on every BIOS write via `resynth_block_crc` over a synthesized gap/mark wire image, an opt-in default-OFF continuous analog head-seek / velocity model `Fds::set_analog_head_seek` replacing the flat `HEAD_RESEEK_CYCLES`, and a BIOS-free synthetic write-verify oracle `Fds::medium_write_verify` as the CI-verifiable half — the real-BIOS write-CRC path stays gitignored/local; an additive **v4** FDS save-state tail, byte-identical with the model off); **peripherals** (a Famicom `$4016`-bit-2 **microphone** `Nes::set_microphone` — `$4016`-only, never `$4017` — and a 3×3-aperture Zapper light-timing model `ZAPPER_APERTURE_*`, both additive / default-off); and a **quality / security** pass (cargo-fuzz targets grown **3 → 8** — `ppu_reg_io` / `apu_reg_io` / `netplay_message` / `save_state` / `movie` — the `movie` target finding and fixing **two real OOM-DoS paths** in `Movie::deserialize`, byte-identical for valid input; a read-only **Tools → ROM Info** browser over `&Nes` that never mutates; and four new MkDocs handbook pages). The prior step — **v2.1.10 "Fathom" ("Loom")** (2026-07-12) — was the **creator-tools & web-parity** step (TAStudio greenzone depth + Lua API breadth + a browser RetroAchievements auth-proxy deploy stack (ADR 0015) + Vs. `DualSystem` presentation in the libretro core; the wasm desktop-style dual present remains deferred). The intervening steps: **v2.1.6 "Timbre"** (2026-07-11) expansion-audio fidelity (a decibel oracle asserting measured level ratios vs Mesen2 / hardware targets, hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical) → **v2.1.7 "Stepping"** (2026-07-12) hardware revisions & DMA frontier (opt-in `PpuRevision` / `Cpu2A03Revision` die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier proven a **documented no-op on every oracle**, ADR 0033 — honest, not faked) → **v2.1.8 "Tempo"** (2026-07-12) performance (a default-OFF specialized fast PPU dot path, `Nes::set_fast_dotloop` — ~+12.3% rendering-heavy, differential-tested bit-identical; a SIMD-validated software blitter where scalar stays default; a wasm 3.99 MiB-gzip size pass) → **v2.1.9 "Aperture"** (2026-07-12) presentation & signal (a raw NTSC composite `rustynes-ppu::raw_signal` core; a marquee CRT shader stack — CRT-Royale / CRT Guest Advanced / Sony Megatron — all naga-validated WGSL; GIF/WAV capture; a live generated-palette preview). Now on the shipped default the Fathom line keeps the deterministic core byte-identical and layers accuracy + display + quality-of-life work atop the v2.0.0 core: **v2.1.0** accuracy remediation (PPU palette backdrop-override, the mapper-tier completion to **51 Core / 95 Curated / 26 BestEffort of 172**, the MMC3 R1/R2 residual closed by-design-permanent) → **v2.1.1** the Wizards & Warriors game-DB mirroring freeze fix → **v2.1.2 "Prism"** display fidelity (Vs. `DualSystem` desktop second-screen, the NTSC composite-shader ladder, an in-core generated NTSC palette, NSF non-60 Hz + NSFe) → **v2.1.3 "Codex"** quality-of-life (an APU audio filter-model selector, Game Genie code nomination + a full / header-robust code database, a Material-for-MkDocs handbook served at `/docs/` on GitHub Pages) → **v2.1.4 "Caliper"** accuracy hardening (opt-in default-OFF OAM decay modeled on Mesen2, a CI boot-smoke sweep of all 26 BestEffort mapper families, a shared MMC3-clone A12/IRQ timing oracle) → **v2.1.5 "Vernier"** the regression-net & residual step → **v2.1.6 "Timbre"** expansion-audio fidelity → **v2.1.7 "Stepping"** hardware revisions & DMA frontier → **v2.1.8 "Tempo"** performance → **v2.1.9 "Aperture"** presentation & signal → **v2.1.10 "Loom"** creator tools & web parity → **v2.2.0 "Capstone"** the milestone cut closing the run → **v2.2.1** this housekeeping patch (this release). v2.0.0 "Timebase" remains RustyNES's designated MAJOR-boundary release — see "Timebase (v2.0.0)" below. RustyNES is now a multi-platform emulation suite, all on the one byte-identical cycle-accurate core — **`docs/STATUS.md` is the authoritative current-state record.** What ships beyond the desktop app: @@ -180,7 +182,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.2.1** (2026-07-15, a housekeeping patch — dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition, zero accuracy/feature/core changes — on top of **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** this housekeeping patch (this release) — 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.2.1 is released** — in particular the joint mobile app-store launch (Google Play + Apple App Store + AltStore PAL + F-Droid) is the future **v2.3.0** (NOT v2.1.0 or v2.2.0 — the entire v2.1.x line and the v2.2.0 "Capstone" milestone have all already shipped, closing the "deepen the existing project" run; the store launch moved out to v2.3.0 — 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.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, AccuracyCoin 141/141 — on top of **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"** this build/distribution/CI-integrity patch (this release) — 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.2.2 is released** — in particular the joint mobile app-store launch (Google Play + Apple App Store + AltStore PAL + F-Droid) is the future **v2.3.0** (NOT v2.1.0 or v2.2.0 — the entire v2.1.x line and the v2.2.0 "Capstone" milestone have all already shipped, closing the "deepen the existing project" run; the store launch moved out to v2.3.0 — 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 vendored `tricnes/` + upstream READMEs, and the frozen `docs/archive/` + `to-dos/archive/` trees — don't lint or reformat those. @@ -197,7 +199,7 @@ These cross-cutting decisions span multiple files. Reading individual chip docs - **`pre-commit run --all-files` REWRITES vendored/immutable trees — use `--files ` or a single named hook instead.** `trailing-whitespace` / `end-of-file-fixer` / `mixed-line-ending` *modify* files and, before PR #320, had no `exclude` at all: one `--all-files` run silently reformatted **41 files** across the vendored TriCNES C#, vendored rcheevos C, `ref-docs/`, an upstream font licence, and upstream test-ROM READMEs — destroying exactly the byte-identical-to-upstream property those trees exist for. `.markdownlintignore` covered them for markdownlint only. #320 added a shared `exclude` anchor across the three rewriting hooks, scoped deliberately **narrower** than `.markdownlintignore`: only content we did not author. Frozen-but-ours trees (`docs/archive/`, `to-dos/plans/`, `docs/monetization/`) stay in scope, since the invariant is "don't rewrite what we didn't write". If it happens anyway, revert **only** the unintended paths (never a blanket `git checkout`, and never including your own edits). - **The libretro buildbot is a THIRD CI system with its own rules — and the pinned toolchain fights it.** The first real run (pipeline #91899, 2026-07-20) passed 1 of 10 jobs. `rust-toolchain.toml`'s `channel = "1.96.0"` makes rustup install a *fresh* toolchain inside libretro's build image, bypassing the image's pre-provisioned cross targets, so 8 jobs died on `E0463: can't find crate for core`; each job in `.gitlab-ci.yml` now runs `rustup target add ${RUST_TARGET}` (NOT added to `rust-toolchain.toml`'s `targets` — that would cost every contributor and GH Actions job ~8 extra `rust-std` downloads). The Apple jobs must use `!reference` rather than `extends` for that, because GitLab's `extends` REPLACES array keys and would silently drop the templates' `SDKROOT`/`STRIP`/`CC`/`CXX` exports. **tvOS: the upstream template's `cargo +nightly build -Zbuild-std` override is OBSOLETE — don't reinstate it.** It dates from when `aarch64-apple-tvos` was tier 3 with no distributed `rust-std`; the target has since been promoted and rustup ships a complete prebuilt std **including `panic_abort`** (verified on the pinned 1.96.0: `rustup target add aarch64-apple-tvos` gives 26 rlibs and the crate `cargo check`s clean, bindgen included). Our job overrides `script` back to `!reference [.libretro-rust-apple-base, script]`, putting tvOS on the same pinned stable as every other job. That one change dissolved THREE stacked workarounds the `+nightly` path had forced: a nightly-channel reinstall (`+nightly` outranks both `rust-toolchain.toml` and `RUSTUP_TOOLCHAIN`, so the job rode the image's stale 1.94.0-nightly, below our MSRV); `CARGO_PROFILE_RELEASE_PANIC=unwind` (bare `-Zbuild-std` omits `panic_abort`, and `CARGO_UNSTABLE_BUILD_STD` does NOT override the hardcoded crate list — the CLI `-Z` flag wins); and clearing the image's `-C ar` (see the next bullet). Worth reporting upstream: every Rust core's tvOS job could drop `+nightly` the same way. **A green GitHub Actions run does not imply a green buildbot** — the new `libretro-cross` CI job (one triple for each buildbot ABI family a Linux runner can model — MinGW-Windows and Android/NDK; the Apple families are deliberately excluded, as bindgen needs a real per-target sysroot and there is no Apple SDK on a Linux runner) is the early-warning gate; before touching anything libretro-related, cross-check `cargo check --release -p rustynes-libretro --target ` locally. - **The libretro build image injects `-C ar` into EVERY Apple job, and it is a hard error from Rust 1.97 — a bomb armed against the next MSRV bump.** The image (not the `rust-apple.yml` template, which sets no `RUSTFLAGS` at all, and not our `.cargo/config.toml`) adds `-Car=,Clink-arg=-undefined,Clink-arg=dynamic_lookup,-rpath=` to osx-x64 / osx-arm64 / ios-arm64 / tvos-arm64. `-C ar` was a deprecated no-op for years and became a **hard error in 1.97** (bisected locally: 1.93.0-nightly / 1.96.0 / 1.96.1 warn; 1.97.1 and 1.99.0-nightly error). No job trips it today — all four Apple jobs are on the pinned 1.96.0 and merely log the warning. **The day `rust-toolchain.toml` moves to 1.97+, all four fail together** — the warning lives in that file, at the line someone would edit. Discarding the flags is behaviour-preserving, not a gamble: rustc splits `-C` at the FIRST `=`, so the whole comma-joined string is swallowed as the `ar` value and those link args have never reached the linker for *any* core (cargo prints it as one argv token), and two upstream Rust cores have green tvOS jobs on the same image with the same dead token. The override works without knowing where the image sets it because cargo takes rustflags from exactly one source, first match wins: `CARGO_ENCODED_RUSTFLAGS` → `RUSTFLAGS` → `target..rustflags` → `build.rustflags` (verified locally against a global `~/.cargo/config.toml` `build.rustflags`: `RUSTFLAGS=""` removes every injected `-C`, and empty means zero flags, not one empty argument). -- **There is ONE toolchain, `rust-toolchain.toml`'s `channel`, and no version literal anywhere in `.github/` — don't add one.** `.github/actions/rust-setup` parses the channel out of that file and fails closed if it can't, so a toolchain bump is a one-line edit there. Pass the composite's `toolchain:` input only to install something *deliberately* different from the project pin. The old `stable` default was misleading rather than wrong: `rust-toolchain.toml` is a directory override that outranks the `rustup default` the action performs, so every job was already compiling on 1.96.0 (rustup logs `overridden by .../rust-toolchain.toml`) — `stable` just downloaded a second toolchain nothing used and made the workflows *read* as though they tested latest stable, which they never did. **Nightly is used in exactly two places, neither a gate:** `cargo fuzz` (hard requirement — libFuzzer's sanitizer flags are nightly-only) and the dormant `rustynes-monetization` crate's standalone `uniffi-bindgen`. If you think a CI job needs nightly, it doesn't. +- **There is ONE toolchain, `rust-toolchain.toml`'s `channel`, and no version literal anywhere in `.github/` — don't add one.** `.github/actions/rust-setup` parses the channel out of that file and fails closed if it can't, so a toolchain bump is a one-line edit there. Pass the composite's `toolchain:` input only to install something *deliberately* different from the project pin. **The resolver is table-scoped `awk` on purpose — do NOT "simplify" it back to a one-line `sed`.** Matching the first `channel = "..."` *anywhere* in the file (the first implementation, caught in review on PR #322) resolves `nightly` if any other table carries a `channel` key ahead of `[toolchain]` — silently installing the very toolchain this setup exists to keep out, while the step still reports success. `awk` rather than `tomllib` because the step runs on Windows and macOS runners too and Python ≥3.11 is not a safe assumption there; only double-quoted TOML strings are accepted, and anything else (missing table, single-quoted value, empty file) aborts the job rather than being guessed at. The old `stable` default was misleading rather than wrong: `rust-toolchain.toml` is a directory override that outranks the `rustup default` the action performs, so every job was already compiling on 1.96.0 (rustup logs `overridden by .../rust-toolchain.toml`) — `stable` just downloaded a second toolchain nothing used and made the workflows *read* as though they tested latest stable, which they never did. **Nightly is used in exactly two places, neither a gate:** `cargo fuzz` (hard requirement — libFuzzer's sanitizer flags are nightly-only) and the dormant `rustynes-monetization` crate's standalone `uniffi-bindgen`. If you think a CI job needs nightly, it doesn't. - **`rust-libretro 0.3.2` is unmaintained (no commit since 2023-02) and has a MinGW bug we work around.** It casts a keycode with `cfg(target_family = "windows")`, but C enum signedness follows the *ABI*: only **MSVC** gives plain enums `int` — under **MinGW** (`x86_64-pc-windows-gnu`, what the buildbot builds) bindgen emits `c_uint` and the crate fails `E0308`. `.cargo/config.toml`'s `[env] BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu = "--target=x86_64-pc-windows-msvc"` fixes it; the generated-bindings diff is 28 lines, all enum signedness. Don't "clean up" that env var without rebuilding for `x86_64-pc-windows-gnu`. - **CodeRabbit is now a 3rd automated PR review bot** (`.coderabbit.yaml`, added 2026-07-20 in PR #316), alongside gemini-code-assist and copilot-pull-request-reviewer — same reply-and-resolve-every-thread ceremony applies before any merge. Configured `profile: assertive` (not the "chill" default) and a `tools{}`/`path_instructions`/custom-checks set audited against this repo's actual file footprint, not guessed. `tone_instructions` has a hard 250-character schema limit that fails validation silently on the CodeRabbit side — after editing `.coderabbit.yaml`, verify with a `@coderabbitai configuration` PR comment and confirm every changed field shows `Source: Repository YAML (base)`. - **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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e20daf..6f4db6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,27 @@ cycle-accurate core later replaced. ## [Unreleased] +## [2.2.2] - 2026-07-21 - "Conduit" (libretro buildbot 10/10 + CI supply-chain hardening + single-source toolchain) + +A **build, distribution, and CI-integrity patch**. It carries RustyNES onto +RetroArch's own buildbot — the recipe now builds **all ten platform jobs**, +after a three-round diagnosis against a third-party pipeline we cannot push to +or re-run — hardens the GitHub Actions supply chain, and collapses the +toolchain to a single pinned source of truth with no `nightly` on any build +path. + +**Zero emulation-core changes.** No file under `crates/rustynes-{cpu,ppu,apu, +mappers,core}` is touched, so the deterministic `#![no_std]` chip stack, +save-state / TAS / netplay-replay formats, and every golden vector are +untouched by construction: **AccuracyCoin holds 141/141 (100.00%)**, nestest +stays 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / +`visual_regression` / the 60-ROM commercial oracle are all unchanged from +v2.2.1. + +One behavioural improvement does reach a shipped artifact: the libretro **tvOS** +core is now built with `panic = "abort"` like every other platform, rather than +the `panic = "unwind"` its previous `-Zbuild-std` path forced. + ### Added - Libretro buildbot CI recipe (`.gitlab-ci.yml`, issue #311) covering Windows diff --git a/Cargo.lock b/Cargo.lock index ce36e02e..fb2d575e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4290,7 +4290,7 @@ dependencies = [ [[package]] name = "rustynes-android" -version = "2.2.1" +version = "2.2.2" dependencies = [ "android-activity", "android_logger", @@ -4308,7 +4308,7 @@ dependencies = [ [[package]] name = "rustynes-apu" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4321,7 +4321,7 @@ dependencies = [ [[package]] name = "rustynes-cheevos" -version = "2.2.1" +version = "2.2.2" dependencies = [ "cc", "ureq", @@ -4329,7 +4329,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4346,7 +4346,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4357,7 +4357,7 @@ dependencies = [ [[package]] name = "rustynes-frontend" -version = "2.2.1" +version = "2.2.2" dependencies = [ "anstyle", "arboard", @@ -4411,11 +4411,11 @@ dependencies = [ [[package]] name = "rustynes-gfx-shaders" -version = "2.2.1" +version = "2.2.2" [[package]] name = "rustynes-hdpack" -version = "2.2.1" +version = "2.2.2" dependencies = [ "lewton", "png", @@ -4426,7 +4426,7 @@ dependencies = [ [[package]] name = "rustynes-ios" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bytemuck", "cpal", @@ -4440,7 +4440,7 @@ dependencies = [ [[package]] name = "rustynes-libretro" -version = "2.2.1" +version = "2.2.2" dependencies = [ "libc", "rust-libretro", @@ -4449,7 +4449,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4461,7 +4461,7 @@ dependencies = [ [[package]] name = "rustynes-mobile" -version = "2.2.1" +version = "2.2.2" dependencies = [ "rustynes-core", "rustynes-hdpack", @@ -4476,14 +4476,14 @@ dependencies = [ [[package]] name = "rustynes-monetization" -version = "2.2.1" +version = "2.2.2" dependencies = [ "uniffi", ] [[package]] name = "rustynes-netplay" -version = "2.2.1" +version = "2.2.2" dependencies = [ "futures-util", "js-sys", @@ -4499,7 +4499,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.2.1" +version = "2.2.2" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4511,14 +4511,14 @@ dependencies = [ [[package]] name = "rustynes-ra" -version = "2.2.1" +version = "2.2.2" dependencies = [ "rustynes-cheevos", ] [[package]] name = "rustynes-script" -version = "2.2.1" +version = "2.2.2" dependencies = [ "mlua", "piccolo", @@ -4529,7 +4529,7 @@ dependencies = [ [[package]] name = "rustynes-test-harness" -version = "2.2.1" +version = "2.2.2" dependencies = [ "insta", "png", diff --git a/Cargo.toml b/Cargo.toml index d639a2aa..f475f01e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ members = [ default-members = ["crates/rustynes-libretro"] [workspace.package] -version = "2.2.1" +version = "2.2.2" edition = "2024" rust-version = "1.96" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 0abd8767..32829540 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Build Status License: MIT OR Apache-2.0 Version Rust: 1.96
+ Build Status License: MIT OR Apache-2.0 Version Rust: 1.96
AccuracyCoin nestest Try in browser
Platform

@@ -775,7 +775,20 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **v2.2.1**, a **housekeeping patch** on top of +RustyNES's current release is **v2.2.2 "Conduit"**, a **build, distribution, +and CI-integrity patch**. It takes the libretro buildbot recipe from 1 of 10 +jobs green to **all ten building** — the last step before RustyNES appears in +RetroArch's built-in core downloader — hardens the GitHub Actions supply chain +(`persist-credentials: false` on all 19 checkouts, a fail-closed release-tag +check, a SHA-pinned toolchain action), and collapses the toolchain to a single +pinned source of truth with **no `nightly` on any build path**. **Zero +emulation-core changes** — nothing under `crates/rustynes-{cpu,ppu,apu,mappers, +core}` is touched, so AccuracyCoin holds **141/141 (100.00%)** and every golden +vector is untouched by construction. The one behavioural improvement in a +shipped artifact: the libretro **tvOS** core is now built with `panic = "abort"` +like every other platform. + +It follows **v2.2.1**, a **housekeeping patch** on top of v2.2.0 "Capstone" (below): archives two batches of dev/research tooling (the Game Genie header-robust re-key's research scripts and a 2A03-revision DMA-divergence probe), consolidates six open Dependabot PRs with **zero diff --git a/docs/STATUS.md b/docs/STATUS.md index 2e5785e9..5fca54a7 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,23 @@ # RustyNES — Project Status Matrix -> **Current release: v2.2.1** (2026-07-15) — a **housekeeping patch** on top of +> **Current release: v2.2.2** (2026-07-21) — **"Conduit"**, a **build, +> distribution, and CI-integrity patch** on top of v2.2.1 (below). It takes the +> libretro buildbot recipe from 1 of 10 jobs green to **all ten building** +> (three rounds of diagnosis against a third-party pipeline we cannot push to or +> re-run), hardens the GitHub Actions supply chain (`persist-credentials: false` +> on all 19 checkouts, a fail-closed release-tag check, a SHA-pinned toolchain +> action), and collapses the toolchain to a **single pinned source of truth** — +> `rust-toolchain.toml` — with **no `nightly` on any build path** and zero +> toolchain version literals under `.github/`. **Zero emulation-core changes**: +> nothing under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so the +> deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay +> formats, and every golden vector are untouched by construction — **AccuracyCoin +> holds 141/141 (100.00%)**, unchanged since v2.2.0. The one behavioural +> improvement reaching a shipped artifact: the libretro **tvOS** core is now +> built with `panic = "abort"` like every other platform, instead of the +> `panic = "unwind"` its previous `-Zbuild-std` path forced. +> +> The prior release, **v2.2.1** (2026-07-15) — a **housekeeping patch** on top of > v2.2.0 "Capstone" (below): archives two batches of dev/research tooling (the > Game Genie header-robust re-key's six research/verification scripts in > `scripts/gg/`, and the 2A03-revision DMA-divergence probe in `scripts/probes/`), From 67df1e93834fd642132d4d539c07def47eee833d Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Tue, 21 Jul 2026 03:22:47 -0400 Subject: [PATCH 2/2] fix(ci): make the release tag check fail closed on a `{}` body Adopts both review findings on the v2.2.2 cut. 1. (Major, real fail-OPEN) The tag-existence check's jq filter, `[.[] | select(.ref == $r)] | length`, returns 0 for a body of `{}`: `.[]` iterates an object's VALUES, of which `{}` has none, so the filter yields an empty list. Zero is exactly the "tag absent" answer, so a malformed body took the RELEASE path -- on the single decision this step exists to get right. Verified against every shape: {} -> exit 0, count 0 <-- the hole {"message":"Not Found"} -> exit 5 "astring" -> exit 5 null -> exit 5 [] -> exit 0, count 0 (correct: absent) Every other malformed shape aborts unaided because `.[]` or `.ref` errors, which is precisely what made `{}` easy to miss and why relying on jq to error on its own was not enough. An explicit `type != "array"` guard now fails closed; re-verified that `[]` still resolves to 0, an exact match to 1, and a prefix-only match (`v2.2.20` against `v2.2.2`) still to 0, so the exact-ref comparison is unaffected. The claim in the release notes and CHANGELOG that "a non-array body aborts the job" was therefore true only for some shapes when written. Both are corrected to describe what the code now actually does. 2. (Minor) Two stale current-release references in AGENTS.md. The version bump updated the operating-note occurrence but missed the narrative paragraph, which still read "-> v2.2.1 this housekeeping patch (this release)", and the platform-additions paragraph, which still called v2.2.1 "the current release". Docs are the spec here, so an internally contradictory AGENTS.md is a defect in its own right. No version change; v2.2.2 is unchanged as the release being cut. --- .github/release-notes/v2.2.2.md | 2 +- .github/workflows/release-auto.yml | 16 +++++++++++++++- AGENTS.md | 4 ++-- CHANGELOG.md | 6 +++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/release-notes/v2.2.2.md b/.github/release-notes/v2.2.2.md index 69194140..c4691a52 100644 --- a/.github/release-notes/v2.2.2.md +++ b/.github/release-notes/v2.2.2.md @@ -58,7 +58,7 @@ Audited rather than applied blanket: `.github/actions/rust-setup` performs no ch `release-auto.yml`'s tag-existence check previously ran `git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1`, collapsing three distinct outcomes — tag present, tag absent, and *lookup failed* — into two, with any non-zero exit read as "absent". A transient network or auth blip would therefore have pushed an already-released version down the release path. -It now queries `gh api .../git/matching-refs/tags/`, chosen over `git/ref/tags/` because it answers "absent" with HTTP 200 and an empty array instead of a 404, so a genuine miss can never be mistaken for an error. `matching-refs` matches by **prefix**, so the exact ref is compared in `jq` — load-bearing, not defensive: `v2.2` prefix-matches two real tags while exact-matching none. Under `set -euo pipefail` a `gh` failure and a non-array body both abort the job. +It now queries `gh api .../git/matching-refs/tags/`, chosen over `git/ref/tags/` because it answers "absent" with HTTP 200 and an empty array instead of a 404, so a genuine miss can never be mistaken for an error. `matching-refs` matches by **prefix**, so the exact ref is compared in `jq` — load-bearing, not defensive: `v2.2` prefix-matches two real tags while exact-matching none. Under `set -euo pipefail` a `gh` failure and a non-array body both abort the job. The non-array case needs an explicit `type != "array"` guard rather than relying on `jq` to error on its own: a body of `{}` makes `.[]` iterate zero object values, so the filter yields an empty list and `length` is `0` — indistinguishable from a genuine "tag absent", which takes the *release* path. Every other malformed shape (a bare string, `null`, an object with entries) does abort unaided, which is precisely what made `{}` easy to miss. ### `dtolnay/rust-toolchain` SHA-pinned diff --git a/.github/workflows/release-auto.yml b/.github/workflows/release-auto.yml index b1e99fa6..3fdef7b0 100644 --- a/.github/workflows/release-auto.yml +++ b/.github/workflows/release-auto.yml @@ -119,8 +119,22 @@ jobs: # credentials, which is what let the checkout above join the rest of # the #318 sweep. refs_json="$(gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${tag}")" + # The `type != "array"` guard is load-bearing, not belt-and-braces. + # Without it, a body of `{}` makes `.[]` iterate zero object VALUES, + # so the filter yields an empty list and `length` is 0 — identical to + # a genuine "tag absent", which takes the RELEASE path. That is a + # fail-OPEN on the one decision this step exists to get right. + # (Other malformed shapes — a bare string, null, an object with + # entries — do abort on their own, because `.[]` or `.ref` errors; + # `{}` is the shape that slips through, which is exactly why an + # explicit type check is needed rather than relying on jq erroring.) count="$(printf '%s\n' "$refs_json" \ - | jq --arg r "refs/tags/${tag}" '[.[] | select(.ref == $r)] | length')" + | jq --arg r "refs/tags/${tag}" ' + if type != "array" then + error("expected a JSON array from git/matching-refs") + else + [.[] | select(.ref == $r)] | length + end')" case "$count" in 0) echo "Version ${version} has no ${tag} tag yet - will release." diff --git a/AGENTS.md b/AGENTS.md index 32083929..dfbbcb58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in p The prior release, **v2.2.1** (2026-07-15), was a **housekeeping patch** on top of v2.2.0 "Capstone" (next paragraph): archives two batches of dev/research tooling (the Game Genie header-robust re-key's six research/verification scripts in `scripts/gg/`, and the 2A03-revision DMA-divergence probe in `scripts/probes/`), consolidates six open Dependabot PRs with **zero source changes** (`pollster` 0.4→1.0, `wide` 0.7→1.5, `tungstenite`/`tokio-tungstenite` 0.29→0.30, `bytemuck`/`cc` patch, `actions/setup-python` v5→v6), and wires four gitignored, `RUSTYNES_FDS_BIOS`-gated smoke tests against `TakuikaNinja`'s FDS `$4023` / mirroring / audio-register / DRAM-watchdog hardware-verification probes (regression insurance for behavior RustyNES already models correctly, not a fix — the `$4030.D1` DRAM-refresh-watchdog probe tracks a known, honest residual RustyNES does not model, per `docs/accuracy-ledger.md`). **Zero accuracy, feature, or core changes** — the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched; **AccuracyCoin holds 141/141 (100.00%)**, unchanged from v2.2.0. -The prior release, **v2.2.0 "Capstone"** (2026-07-12), was the **milestone cut** that **closes the v2.1.5 → v2.2.0 "deepen the existing project"** run, landing its two remaining marquees — the **netplay matchmaking / lobby** stack and the **FDS medium model** — atop a peripherals + quality/security pass, all sitting on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. Every v2.2.0 change is **additive or default-off**, so the deterministic core is untouched on the shipped default — **AccuracyCoin holds 141/141 (100.00%)**, nestest is 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle / the `#![no_std]` chip stack are all unchanged, and save-state / TAS / netplay replay stay bit-identical. It lands (PRs #290, #291; cut #297): a **netplay lobby + matchmaking** stack over the existing room-code / TURN transport (`ListRooms`/`RoomList` browse-and-join room directory + a server-side `QuickMatch`/`Matched` quick-play path, both **signaling only** with the `room-list` frame parsed by a brace-depth walk bounded at `MAX_ROOM_LIST` = 256); **delayed-stream spectators** (`SpectatorConfig.delay_frames`, clamped to `MAX_DELAY_FRAMES` = 512 — a pure presentation hold that cannot perturb the match); a **hardened desync + peer-liveness surface** (a graded `DesyncStatus` with a hysteresis threshold of 3 ≈ 1.5 s + a graded `PeerLink` / terminal `DisconnectReason::PeerTimeout` on multi-second 2 s / 5 s RTT timeouts — telemetry only, the rollback/determinism contract untouched); the **FDS medium model completion** (F4.3, a marquee — a per-block **CRC-16/KERMIT** re-emitted on every BIOS write via `resynth_block_crc` over a synthesized gap/mark wire image, an opt-in default-OFF continuous analog head-seek / velocity model `Fds::set_analog_head_seek` replacing the flat `HEAD_RESEEK_CYCLES`, and a BIOS-free synthetic write-verify oracle `Fds::medium_write_verify` as the CI-verifiable half — the real-BIOS write-CRC path stays gitignored/local; an additive **v4** FDS save-state tail, byte-identical with the model off); **peripherals** (a Famicom `$4016`-bit-2 **microphone** `Nes::set_microphone` — `$4016`-only, never `$4017` — and a 3×3-aperture Zapper light-timing model `ZAPPER_APERTURE_*`, both additive / default-off); and a **quality / security** pass (cargo-fuzz targets grown **3 → 8** — `ppu_reg_io` / `apu_reg_io` / `netplay_message` / `save_state` / `movie` — the `movie` target finding and fixing **two real OOM-DoS paths** in `Movie::deserialize`, byte-identical for valid input; a read-only **Tools → ROM Info** browser over `&Nes` that never mutates; and four new MkDocs handbook pages). The prior step — **v2.1.10 "Fathom" ("Loom")** (2026-07-12) — was the **creator-tools & web-parity** step (TAStudio greenzone depth + Lua API breadth + a browser RetroAchievements auth-proxy deploy stack (ADR 0015) + Vs. `DualSystem` presentation in the libretro core; the wasm desktop-style dual present remains deferred). The intervening steps: **v2.1.6 "Timbre"** (2026-07-11) expansion-audio fidelity (a decibel oracle asserting measured level ratios vs Mesen2 / hardware targets, hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical) → **v2.1.7 "Stepping"** (2026-07-12) hardware revisions & DMA frontier (opt-in `PpuRevision` / `Cpu2A03Revision` die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier proven a **documented no-op on every oracle**, ADR 0033 — honest, not faked) → **v2.1.8 "Tempo"** (2026-07-12) performance (a default-OFF specialized fast PPU dot path, `Nes::set_fast_dotloop` — ~+12.3% rendering-heavy, differential-tested bit-identical; a SIMD-validated software blitter where scalar stays default; a wasm 3.99 MiB-gzip size pass) → **v2.1.9 "Aperture"** (2026-07-12) presentation & signal (a raw NTSC composite `rustynes-ppu::raw_signal` core; a marquee CRT shader stack — CRT-Royale / CRT Guest Advanced / Sony Megatron — all naga-validated WGSL; GIF/WAV capture; a live generated-palette preview). Now on the shipped default the Fathom line keeps the deterministic core byte-identical and layers accuracy + display + quality-of-life work atop the v2.0.0 core: **v2.1.0** accuracy remediation (PPU palette backdrop-override, the mapper-tier completion to **51 Core / 95 Curated / 26 BestEffort of 172**, the MMC3 R1/R2 residual closed by-design-permanent) → **v2.1.1** the Wizards & Warriors game-DB mirroring freeze fix → **v2.1.2 "Prism"** display fidelity (Vs. `DualSystem` desktop second-screen, the NTSC composite-shader ladder, an in-core generated NTSC palette, NSF non-60 Hz + NSFe) → **v2.1.3 "Codex"** quality-of-life (an APU audio filter-model selector, Game Genie code nomination + a full / header-robust code database, a Material-for-MkDocs handbook served at `/docs/` on GitHub Pages) → **v2.1.4 "Caliper"** accuracy hardening (opt-in default-OFF OAM decay modeled on Mesen2, a CI boot-smoke sweep of all 26 BestEffort mapper families, a shared MMC3-clone A12/IRQ timing oracle) → **v2.1.5 "Vernier"** the regression-net & residual step → **v2.1.6 "Timbre"** expansion-audio fidelity → **v2.1.7 "Stepping"** hardware revisions & DMA frontier → **v2.1.8 "Tempo"** performance → **v2.1.9 "Aperture"** presentation & signal → **v2.1.10 "Loom"** creator tools & web parity → **v2.2.0 "Capstone"** the milestone cut closing the run → **v2.2.1** this housekeeping patch (this release). v2.0.0 "Timebase" remains RustyNES's designated MAJOR-boundary release — see "Timebase (v2.0.0)" below. RustyNES is now a multi-platform emulation suite, all on the one byte-identical cycle-accurate core — **`docs/STATUS.md` is the authoritative current-state record.** What ships beyond the desktop app: +The prior release, **v2.2.0 "Capstone"** (2026-07-12), was the **milestone cut** that **closes the v2.1.5 → v2.2.0 "deepen the existing project"** run, landing its two remaining marquees — the **netplay matchmaking / lobby** stack and the **FDS medium model** — atop a peripherals + quality/security pass, all sitting on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. Every v2.2.0 change is **additive or default-off**, so the deterministic core is untouched on the shipped default — **AccuracyCoin holds 141/141 (100.00%)**, nestest is 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle / the `#![no_std]` chip stack are all unchanged, and save-state / TAS / netplay replay stay bit-identical. It lands (PRs #290, #291; cut #297): a **netplay lobby + matchmaking** stack over the existing room-code / TURN transport (`ListRooms`/`RoomList` browse-and-join room directory + a server-side `QuickMatch`/`Matched` quick-play path, both **signaling only** with the `room-list` frame parsed by a brace-depth walk bounded at `MAX_ROOM_LIST` = 256); **delayed-stream spectators** (`SpectatorConfig.delay_frames`, clamped to `MAX_DELAY_FRAMES` = 512 — a pure presentation hold that cannot perturb the match); a **hardened desync + peer-liveness surface** (a graded `DesyncStatus` with a hysteresis threshold of 3 ≈ 1.5 s + a graded `PeerLink` / terminal `DisconnectReason::PeerTimeout` on multi-second 2 s / 5 s RTT timeouts — telemetry only, the rollback/determinism contract untouched); the **FDS medium model completion** (F4.3, a marquee — a per-block **CRC-16/KERMIT** re-emitted on every BIOS write via `resynth_block_crc` over a synthesized gap/mark wire image, an opt-in default-OFF continuous analog head-seek / velocity model `Fds::set_analog_head_seek` replacing the flat `HEAD_RESEEK_CYCLES`, and a BIOS-free synthetic write-verify oracle `Fds::medium_write_verify` as the CI-verifiable half — the real-BIOS write-CRC path stays gitignored/local; an additive **v4** FDS save-state tail, byte-identical with the model off); **peripherals** (a Famicom `$4016`-bit-2 **microphone** `Nes::set_microphone` — `$4016`-only, never `$4017` — and a 3×3-aperture Zapper light-timing model `ZAPPER_APERTURE_*`, both additive / default-off); and a **quality / security** pass (cargo-fuzz targets grown **3 → 8** — `ppu_reg_io` / `apu_reg_io` / `netplay_message` / `save_state` / `movie` — the `movie` target finding and fixing **two real OOM-DoS paths** in `Movie::deserialize`, byte-identical for valid input; a read-only **Tools → ROM Info** browser over `&Nes` that never mutates; and four new MkDocs handbook pages). The prior step — **v2.1.10 "Fathom" ("Loom")** (2026-07-12) — was the **creator-tools & web-parity** step (TAStudio greenzone depth + Lua API breadth + a browser RetroAchievements auth-proxy deploy stack (ADR 0015) + Vs. `DualSystem` presentation in the libretro core; the wasm desktop-style dual present remains deferred). The intervening steps: **v2.1.6 "Timbre"** (2026-07-11) expansion-audio fidelity (a decibel oracle asserting measured level ratios vs Mesen2 / hardware targets, hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical) → **v2.1.7 "Stepping"** (2026-07-12) hardware revisions & DMA frontier (opt-in `PpuRevision` / `Cpu2A03Revision` die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier proven a **documented no-op on every oracle**, ADR 0033 — honest, not faked) → **v2.1.8 "Tempo"** (2026-07-12) performance (a default-OFF specialized fast PPU dot path, `Nes::set_fast_dotloop` — ~+12.3% rendering-heavy, differential-tested bit-identical; a SIMD-validated software blitter where scalar stays default; a wasm 3.99 MiB-gzip size pass) → **v2.1.9 "Aperture"** (2026-07-12) presentation & signal (a raw NTSC composite `rustynes-ppu::raw_signal` core; a marquee CRT shader stack — CRT-Royale / CRT Guest Advanced / Sony Megatron — all naga-validated WGSL; GIF/WAV capture; a live generated-palette preview). Now on the shipped default the Fathom line keeps the deterministic core byte-identical and layers accuracy + display + quality-of-life work atop the v2.0.0 core: **v2.1.0** accuracy remediation (PPU palette backdrop-override, the mapper-tier completion to **51 Core / 95 Curated / 26 BestEffort of 172**, the MMC3 R1/R2 residual closed by-design-permanent) → **v2.1.1** the Wizards & Warriors game-DB mirroring freeze fix → **v2.1.2 "Prism"** display fidelity (Vs. `DualSystem` desktop second-screen, the NTSC composite-shader ladder, an in-core generated NTSC palette, NSF non-60 Hz + NSFe) → **v2.1.3 "Codex"** quality-of-life (an APU audio filter-model selector, Game Genie code nomination + a full / header-robust code database, a Material-for-MkDocs handbook served at `/docs/` on GitHub Pages) → **v2.1.4 "Caliper"** accuracy hardening (opt-in default-OFF OAM decay modeled on Mesen2, a CI boot-smoke sweep of all 26 BestEffort mapper families, a shared MMC3-clone A12/IRQ timing oracle) → **v2.1.5 "Vernier"** the regression-net & residual step → **v2.1.6 "Timbre"** expansion-audio fidelity → **v2.1.7 "Stepping"** hardware revisions & DMA frontier → **v2.1.8 "Tempo"** performance → **v2.1.9 "Aperture"** presentation & signal → **v2.1.10 "Loom"** creator tools & web parity → **v2.2.0 "Capstone"** the milestone cut closing the run → v2.2.1 (housekeeping) → **v2.2.2 "Conduit"** this build/distribution/CI-integrity patch (this release). v2.0.0 "Timebase" remains RustyNES's designated MAJOR-boundary release — see "Timebase (v2.0.0)" below. RustyNES is now a multi-platform emulation suite, all on the one byte-identical cycle-accurate core — **`docs/STATUS.md` is the authoritative current-state record.** What ships beyond the desktop app: - **Timebase (v2.0.0)** — the scheduler substrate is rewritten from a five-counter dot-lockstep model to a single canonical cycle counter, every CPU cycle a real bus access, and a split-around-the-access `start_cycle`/`end_cycle` PPU catch-up (ADR 0002 / ADR 0029), now the *only* scheduler path. This is a MAJOR-boundary breaking change (ADR 0003): `.rns` save-state and `.rnm` movie format epochs bump (ADR 0028) — a pre-v2.0.0 `.rns` slot now fails to load with a clear error instead of silently misinterpreting stale bytes. Landed across five betas + rc.1 (PRs #217–223). Also new: core-level **Vs. `DualSystem`** dual-console support (`Emu::Dual`, `crates/rustynes-core`) for the four Vs. arcade cabinet boards — core-and-test-harness-only, frontend wiring deferred. The R1/R2 MMC3 IRQ-timing residual is by-design-deferred beyond this release with a mechanism-level finding recorded in ADR 0002 (not closed, not silently dropped). **AccuracyCoin now measures 141/141 (100.00%)**: the v2.0.1 upstream AccuracyCoin re-sync grew the catalog to 146 rows / 141 assigned tests and briefly opened two new PPU gaps ("ALE + Read" $0491, "Hybrid Addresses" $0492), which **v2.0.3** closed by promoting the 2-cycle-ALE PPU fetch model to the unconditional default (both experimental flags retired; additive `PPU_SNAPSHOT_VERSION` v5 tail). AccuracyCoin held 100% (139/139) throughout the v2.0.0 betas and final cut, dipped to 139/141 under the v2.0.1 re-sync, and is back to a full 141/141 from v2.0.3 onward. @@ -40,7 +40,7 @@ The prior release, **v2.2.0 "Capstone"** (2026-07-12), was the **milestone cut** - **Mapper breadth → 172 families** (up from 168 at the v1.7.x tag), Core / Curated / BestEffort behind the CI accuracy-honesty gate. - **Release automation** — `.github/workflows/release-auto.yml`: when a new version goes final-green on `main`, it auto-tags + publishes the GitHub Release (body from a maintainer-authored `.github/release-notes/vX.Y.Z.md` override, else the CHANGELOG `[X.Y.Z]` section; title codename parsed from the CHANGELOG header) and builds + attaches the desktop binaries by invoking `release.yml` via `workflow_call` (a tag pushed by `GITHUB_TOKEN` can't trigger `on: push: tags`, hence the direct call). The v1.8.0–v1.9.9 GitHub Releases are all published with comprehensive notes + Linux / macOS-aarch64 / Windows binaries. -Platform additions through v1.10.0 were **host-only and additive**: the deterministic `#![no_std]` chip stack was untouched and byte-identical on ARM. **v2.0.0 "Timebase" is different by design** — it rewrites the scheduler substrate itself (still `#![no_std]`-clean, AccuracyCoin now back at a full **141/141 (100%)** from v2.0.3 — see above, but the save-state / movie format epochs deliberately bump per ADR 0028, so cross-version `.rns`/`.rnm` round-trip is a v1.x-only guarantee, not a v1.x⇄v2.x one). Forward path: the **v2.0.x "Harbor" mobile-finalization re-port train** onto the v2.0.0 core has fully shipped — v2.0.1 (first Android re-port + AccuracyCoin oracle re-sync), v2.0.2–v2.0.3 (the 2-cycle-ALE accuracy closure to 141/141), v2.0.4 (Android release candidate), v2.0.5–v2.0.8 (iOS finalization), and v2.0.9 (both-apps readiness) — followed by the **v2.1.x "Fathom" accuracy line** (v2.1.0 → v2.1.10) capped by the **v2.2.0 "Capstone"** milestone cut, then the **v2.2.1** housekeeping patch, the current release; see the "Current release" paragraph above. The **v2.1.5 → v2.2.0** line is a **"deepen the existing project"** run (accuracy / performance / features / quality); **v2.1.5 "Vernier"** opened it (the tepples Holy Mapperel mapper bank-reachability / IRQ regression net, the first PAL-region APU oracle at `pal_apu_tests` 10/10, the MMC3 R1/R2 F5.0 A12-phase study, a measured fat-LTO A/B, and a real TURN NAT-traversal retransmit production fix — all NTSC-byte-identical), **v2.1.6 "Timbre"** continued it (the expansion-audio decibel oracle, the hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, and a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical), **v2.1.7 "Stepping"** added opt-in PPU / 2A03 die-revisions + power-on RAM/palette hardware models (the DMA "unexpected read" frontier proven a documented no-op on every oracle, ADR 0033 — honest, not faked), **v2.1.8 "Tempo"** the default-OFF specialized fast PPU dot path (~12% rendering-heavy, differential-tested bit-identical) + a SIMD-validated software blitter + a wasm size pass, **v2.1.9 "Aperture"** the marquee CRT shader stack + a raw NTSC composite signal-decode path + GIF/WAV capture + a palette editor, **v2.1.10 "Loom"** the TAStudio greenzone + Lua API breadth + the browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation, and **v2.2.0 "Capstone"** the milestone cut closing the run (the netplay matchmaking / lobby stack + the FDS medium model + a peripherals & quality/security pass — fuzz targets 3 → 8, a `Movie::deserialize` OOM-DoS fix, a read-only Tools → ROM Info browser) — all NTSC-byte-identical, AccuracyCoin 141/141 throughout; the v2.1.5 → v2.2.0 run is now closed. The **joint Google Play + Apple App Store + AltStore PAL + F-Droid launch** — with it the `rustynes-monetization` activation — is the future **v2.3.0** (moved from the earlier v2.1.0 / v2.2.0 targets). +Platform additions through v1.10.0 were **host-only and additive**: the deterministic `#![no_std]` chip stack was untouched and byte-identical on ARM. **v2.0.0 "Timebase" is different by design** — it rewrites the scheduler substrate itself (still `#![no_std]`-clean, AccuracyCoin now back at a full **141/141 (100%)** from v2.0.3 — see above, but the save-state / movie format epochs deliberately bump per ADR 0028, so cross-version `.rns`/`.rnm` round-trip is a v1.x-only guarantee, not a v1.x⇄v2.x one). Forward path: the **v2.0.x "Harbor" mobile-finalization re-port train** onto the v2.0.0 core has fully shipped — v2.0.1 (first Android re-port + AccuracyCoin oracle re-sync), v2.0.2–v2.0.3 (the 2-cycle-ALE accuracy closure to 141/141), v2.0.4 (Android release candidate), v2.0.5–v2.0.8 (iOS finalization), and v2.0.9 (both-apps readiness) — followed by the **v2.1.x "Fathom" accuracy line** (v2.1.0 → v2.1.10) capped by the **v2.2.0 "Capstone"** milestone cut, then the v2.2.1 housekeeping patch and **v2.2.2 "Conduit"**, the current release; see the "Current release" paragraph above. The **v2.1.5 → v2.2.0** line is a **"deepen the existing project"** run (accuracy / performance / features / quality); **v2.1.5 "Vernier"** opened it (the tepples Holy Mapperel mapper bank-reachability / IRQ regression net, the first PAL-region APU oracle at `pal_apu_tests` 10/10, the MMC3 R1/R2 F5.0 A12-phase study, a measured fat-LTO A/B, and a real TURN NAT-traversal retransmit production fix — all NTSC-byte-identical), **v2.1.6 "Timbre"** continued it (the expansion-audio decibel oracle, the hardware/Mesen2 channel-level calibration incl. the Namco 163 ~12 dB fix, VRC7 patch-set verification vs Nuke.YKT, and a frontend Audio Mixer panel — base 2A03 NTSC output byte-identical), **v2.1.7 "Stepping"** added opt-in PPU / 2A03 die-revisions + power-on RAM/palette hardware models (the DMA "unexpected read" frontier proven a documented no-op on every oracle, ADR 0033 — honest, not faked), **v2.1.8 "Tempo"** the default-OFF specialized fast PPU dot path (~12% rendering-heavy, differential-tested bit-identical) + a SIMD-validated software blitter + a wasm size pass, **v2.1.9 "Aperture"** the marquee CRT shader stack + a raw NTSC composite signal-decode path + GIF/WAV capture + a palette editor, **v2.1.10 "Loom"** the TAStudio greenzone + Lua API breadth + the browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation, and **v2.2.0 "Capstone"** the milestone cut closing the run (the netplay matchmaking / lobby stack + the FDS medium model + a peripherals & quality/security pass — fuzz targets 3 → 8, a `Movie::deserialize` OOM-DoS fix, a read-only Tools → ROM Info browser) — all NTSC-byte-identical, AccuracyCoin 141/141 throughout; the v2.1.5 → v2.2.0 run is now closed. The **joint Google Play + Apple App Store + AltStore PAL + F-Droid launch** — with it the `rustynes-monetization` activation — is the future **v2.3.0** (moved from the earlier v2.1.0 / v2.2.0 targets). --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4db6bd..71b1508e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,11 @@ the `panic = "unwind"` its previous `-Zbuild-std` path forced. is needed. That endpoint matches by prefix, so the exact ref is compared in `jq` — verified necessary, not theoretical: `v2.2` prefix-matches two real tags while exact-matching none. Every failure path now aborts the job under - `set -euo pipefail` instead of resolving to a release decision. Removing + `set -euo pipefail` instead of resolving to a release decision — including + the one shape that slipped through review-round one: an explicit + `type != "array"` guard, because a body of `{}` makes `.[]` iterate zero + object values, so the filter returns `0` and is indistinguishable from a + genuine "tag absent", which takes the *release* path. Removing the last Git operation is also what let that job's checkout — the only one that had needed the credential — join the sweep. - `.github/actions/rust-setup` pinned `dtolnay/rust-toolchain` from `@master`