diff --git a/.github/release-notes/v2.2.0.md b/.github/release-notes/v2.2.0.md new file mode 100644 index 00000000..47a25a02 --- /dev/null +++ b/.github/release-notes/v2.2.0.md @@ -0,0 +1,41 @@ +# RustyNES v2.2.0 — Capstone + +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** — alongside a peripherals + quality/security pass. Every change is additive or default-off: the deterministic core is untouched, so **AccuracyCoin holds 141/141 (100.00%)**, `visual_regression` is byte-identical, nestest is 0-diff, `pal_apu_tests` 10/10, and save-state / TAS / netplay replay stay bit-identical. Two PRs (#290, #291). + +## Added + +### Netplay matchmaking + lobby, spectators, desync/liveness hardening (B5) — #290 + +- **Lobby + matchmaking** — the pure signaling protocol (`crates/rustynes-netplay/src/signaling.rs`) grows a **browse-and-join** lobby directory and a matchmaking path atop the existing room-code / TURN stack. New `SignalMessage` variants — `ListRooms { rom_hash }` → `RoomList { rooms }` (open, joinable, optionally game-filtered rooms; each `RoomInfo` carries code / player-count / capacity / `rom_hash` and *no* SDP/ICE/identity), and `QuickMatch { rom_hash, max_players }` → `Matched { room, slot, max_players }` (server-side quick-play: join any open room for the ROM via the shared `add_to_room` primitive, or mint a fresh `QM-NNNNNN` room). The `room-list` frame is parsed by a brace-depth walk bounded at `MAX_ROOM_LIST` (256) so an oversized frame cannot force an unbounded allocation. Determinism / rollback contract untouched — signaling only. +- **Delayed-stream spectators** — `SpectatorConfig.delay_frames` (clamped to `MAX_DELAY_FRAMES` = 512 ≈ 8.5 s) layers an intentional broadcast / anti-spoiler / jitter-smoothing hold atop the natural spectator lag: frame `f` is revealed only once frame `f + delay_frames` is confirmed (`reveal_horizon()`). Purely a *presentation* delay — frames are still produced byte-identically and in order and the spectator still sends nothing — so it cannot perturb the match. Wired to `NetplayUi::spectator_delay_frames` (default 0). +- **Hardened desync surface** — `DesyncDiagnostics` gains a single graded `DesyncStatus` { `InSync` / `Suspect` / `Desynced` } verdict with a hysteresis threshold (`DEFAULT_DESYNC_THRESHOLD` = 3 consecutive mismatches ≈ 1.5 s at the 30-frame checksum interval) so a lone reordered / late peer checksum no longer flashes a false desync banner, plus a sticky peak-run rule so a confirmed desync never silently downgrades. Pure telemetry over the `NetMessage::Checksum` digests the session already exchanges. +- **Peer-liveness RTT timeouts** — a graded `PeerLink` { `Live` / `Interrupted` / `TimedOut` } for an already-synced `NetplayConnection`, driven by `last_recv` against `peer_interrupt_timeout` (2 s) / `peer_disconnect_timeout` (5 s), plus a terminal `DisconnectReason::PeerTimeout`. Deliberately far above Mesen's trigger-happy ~150 ms: a single lost 1 Hz `Quality` ping or a routine Wi-Fi/LTE retransmit spike never trips it, matching the multi-second grace windows GGPO/Parsec use. Both thresholds are builder-configurable (`with_peer_timeouts`). + +### FDS medium model completion — CRC-16 / gap / continuous head-seek (F4.3, a marquee) — #291 + +- The Famicom Disk System RAM adapter (`crates/rustynes-mappers/src/fds.rs`) completes the disk **medium** model. The disk is a synthesized byte-stream wire image — lead-in / inter-block gaps, a `$80` start mark, the block bytes, and a **CRC-16/KERMIT** per block — and each BIOS-written block now **re-emits a fresh per-block CRC-16** over its updated payload (`resynth_block_crc`), modelling the RP2C33 controller's continuous CRC generator so the medium stays self-consistent after a write. +- A new **continuous analog head-seek / velocity model** (opt-in, default-OFF — `Fds::set_analog_head_seek`) replaces the flat fixed `HEAD_RESEEK_CYCLES` motor-restart not-ready window with a belt-driven, distance-proportional seek time (`HEAD_SEEK_BYTES_PER_CYCLE` velocity + `HEAD_SEEK_SETTLE_CYCLES` settle, clamped to a cold spin-up), sized from the head-travel distance captured at motor-off. +- A **BIOS-free synthetic write-verify oracle** (`Fds::medium_write_verify`) walks the wire image and asserts every block's CRC-16 and gap/mark framing round-trips — the CI-verifiable half of the medium model; the real-BIOS write-CRC path needs a copyright `disksys.rom` and runs only from a gitignored local dump (`docs/accuracy-ledger.md` records the CI-verifiable-vs-local-only split). **Additive and deterministic**: with the head-seek model off (the default) a non-writing `.fds` run is **byte-identical** to prior releases; the new state round-trips an additive **v4** FDS save-state tail (v1/v2/v3 blobs load with the model disabled). AccuracyCoin has no FDS ROM, so **141/141** is unaffected. + +### Peripherals — Famicom microphone + Zapper light-timing (peripherals) — #291 + +- The Famicom built-in controller-2 **microphone** is modelled on **`$4016` bit 2** (`Nes::set_microphone` / `Bus::set_microphone`), wired through the frontend input path (hold-to-talk `M` key → `FrameInputs.microphone` → latch), for games such as *The Legend of Zelda* (Pols Voice) and *Kid Icarus*. It is a `$4016`-only signal (never touches `$4017`). +- The **Zapper** photodiode now integrates a **3×3 aperture** (field-of-view) around the aim point, asserting light only when ≥2 pixels cross the luma threshold (`ZAPPER_APERTURE_*`) — hardening detection against sub-pixel aim error and PPU edge noise vs the prior single-pixel sample, while staying a deterministic pure function of the presented framebuffer (no save-state change). Both are additive and **default-off**. + +### Quality / security — fuzz expansion, ROM Info browser, handbook (quality) — #290 + +- **Fuzz-target expansion** — `fuzz/` grows from 3 to 8 cargo-fuzz targets covering the remaining untrusted-input boundaries: `ppu_reg_io`, `apu_reg_io`, `netplay_message` (the highest-value target — `NetMessage::from_bytes` binary UDP + `SignalMessage::parse` JSON signaling/lobby, both ingesting bytes straight off the wire), `save_state` (`parse_header` + `extract_thumbnail` + `restore_quiet`), and `movie` (`Movie::deserialize`). Each builds under nightly cargo-fuzz and runs clean for tens of thousands of iterations. The `movie` target **found two real OOM-DoS paths** (see Changed). +- **Read-only ROM Info browser** — a new **Tools → ROM Info** panel surfaces, for the loaded ROM, the two dump-identity CRC32 keys (the header-excluded game-DB key + the full-file **No-Intro** key), the SHA-256, the effective per-game database entry, and the decoded cartridge header read straight off the running `Nes`. Read-only (`&Nes`) — never mutates the emulator or the DB overlay; the deterministic core never consults it. +- **MkDocs handbook deepening** — four new Material-for-MkDocs pages (`docs/expansion-audio.md`, `docs/pal-region.md`, `docs/crt-composite.md`, `docs/creator-tools.md`) — curated entry points for the newer subsystems, cross-linked to the authoritative specs, with matching `mkdocs.yml` nav entries. + +## Changed + +- **Movie (`.rnm`) deserializer hardening** — the new `movie` fuzz target surfaced two OOM-DoS paths in `Movie::deserialize` (`crates/rustynes-core/src/movie.rs`), both now fixed **byte-identically for valid input**: (1) the untrusted 4-byte `frame_count` was passed straight to `Vec::with_capacity`, so a 49-byte header could claim a multi-gigabyte reservation — now capped at `remaining_bytes / width`; (2) a `bytes_per_frame` of 0 made each `r.take(0)` consume no input, so the frame loop pushed `frame_count` empty records out of a finite file — now rejected up front. Regression test `deserialize_hostile_frame_count_does_not_oom` added; the existing 44 movie tests (incl. the determinism round-trip) stay green. + +## Notes + +- Version: workspace `2.1.10 → 2.2.0` (Cargo.lock refreshed). No dependency or mobile changes (the mobile apps re-release at v2.3.0). +- **No default-build behavior change.** Netplay lobby / spectators / desync / liveness are signaling + telemetry only; the FDS head-seek model and both peripherals are default-OFF; the ROM Info browser is read-only. AccuracyCoin stays **141/141 (100.00%)**, `visual_regression` byte-identical, `pal_apu_tests` 10/10, and save-state / TAS / netplay replay stay bit-identical (verified by the netplay rollback-determinism + snapshot-restore-replay + FDS synthetic write-verify gates). +- **Review-surfaced fixes landed alongside:** a netplay **liveness-spoofing** fix (`last_recv` refreshes only for the adopted peer's source, so a third party can no longer suppress `PeerTimeout`); `QuickMatch` now scans `self.rooms` uncapped (was capped at `MAX_ROOM_LIST`, contradicting its own contract); the FDS DMC-conflict `$4016` read now ORs the mic bit (`mic << 2`); and the FDS `load_disk_tail` offset carries a `debug_assert_eq!` full-consume invariant. +- Docs updated in-change: `docs/frontend.md`, `docs/mappers.md`, `docs/accuracy-ledger.md`, the four new handbook pages, `fuzz/README.md`, `CHANGELOG.md`. +- Full host gate green: fmt · clippy (native all-targets + `scripting` / `scripting,hd-pack` / `retroachievements` + all wasm32 targets) · rustdoc (`-D warnings`) · markdownlint (pinned v0.39.0) · AccuracyCoin 141/141 + `visual_regression` byte-identical + nestest 0-diff + `pal_apu_tests` 10/10 + the netplay determinism / FDS write-verify suites · `no_std` `thumbv7em` cross-compile · all 8 cargo-fuzz targets build · version consistency. diff --git a/CHANGELOG.md b/CHANGELOG.md index d602250c..d9266b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ cycle-accurate core later replaced. ## [Unreleased] +## [2.2.0] - 2026-07-12 - "Capstone" (milestone cut — netplay matchmaking/lobby + FDS medium model + peripherals + quality/security pass) + ### Added - **Netplay lobby + matchmaking (v2.2.0 "Capstone", B5).** The pure signaling protocol (`crates/rustynes-netplay/src/signaling.rs`) grows a **browse-and-join** lobby directory and a matchmaking path atop the existing room-code / TURN stack. New `SignalMessage` variants — `ListRooms { rom_hash }` → `RoomList { rooms: Vec }` (the open, joinable, optionally game-filtered rooms; each `RoomInfo` carries the code / player count / capacity / `rom_hash` and *no* SDP/ICE/identity), and `QuickMatch { rom_hash, max_players }` → `Matched { room, slot, max_players }` (server-side "quick play": join any open room for the ROM via the shared `add_to_room` primitive, or create a fresh one with a deterministic `QM-NNNNNN` code). The `room-list` JSON array is parsed by a brace-depth walk bounded at `MAX_ROOM_LIST` (256) so an oversized frame cannot force an unbounded allocation. Determinism/rollback contract untouched — this is signaling only. diff --git a/Cargo.lock b/Cargo.lock index 6c06c45c..d1f9185e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "rustynes-android" -version = "2.1.10" +version = "2.2.0" dependencies = [ "android-activity", "android_logger", @@ -4266,7 +4266,7 @@ dependencies = [ [[package]] name = "rustynes-apu" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4279,7 +4279,7 @@ dependencies = [ [[package]] name = "rustynes-cheevos" -version = "2.1.10" +version = "2.2.0" dependencies = [ "cc", "ureq", @@ -4287,7 +4287,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4304,7 +4304,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4315,7 +4315,7 @@ dependencies = [ [[package]] name = "rustynes-frontend" -version = "2.1.10" +version = "2.2.0" dependencies = [ "anstyle", "arboard", @@ -4369,11 +4369,11 @@ dependencies = [ [[package]] name = "rustynes-gfx-shaders" -version = "2.1.10" +version = "2.2.0" [[package]] name = "rustynes-hdpack" -version = "2.1.10" +version = "2.2.0" dependencies = [ "lewton", "png", @@ -4384,7 +4384,7 @@ dependencies = [ [[package]] name = "rustynes-ios" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bytemuck", "cpal", @@ -4398,7 +4398,7 @@ dependencies = [ [[package]] name = "rustynes-libretro" -version = "2.1.10" +version = "2.2.0" dependencies = [ "libc", "rust-libretro", @@ -4407,7 +4407,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4419,7 +4419,7 @@ dependencies = [ [[package]] name = "rustynes-mobile" -version = "2.1.10" +version = "2.2.0" dependencies = [ "rustynes-core", "rustynes-hdpack", @@ -4434,14 +4434,14 @@ dependencies = [ [[package]] name = "rustynes-monetization" -version = "2.1.10" +version = "2.2.0" dependencies = [ "uniffi", ] [[package]] name = "rustynes-netplay" -version = "2.1.10" +version = "2.2.0" dependencies = [ "futures-util", "js-sys", @@ -4457,7 +4457,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.1.10" +version = "2.2.0" dependencies = [ "bitflags 2.13.0", "criterion", @@ -4469,14 +4469,14 @@ dependencies = [ [[package]] name = "rustynes-ra" -version = "2.1.10" +version = "2.2.0" dependencies = [ "rustynes-cheevos", ] [[package]] name = "rustynes-script" -version = "2.1.10" +version = "2.2.0" dependencies = [ "mlua", "piccolo", @@ -4487,7 +4487,7 @@ dependencies = [ [[package]] name = "rustynes-test-harness" -version = "2.1.10" +version = "2.2.0" dependencies = [ "insta", "png", diff --git a/Cargo.toml b/Cargo.toml index db4c3a12..8c263c95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ ] [workspace.package] -version = "2.1.10" +version = "2.2.0" edition = "2024" rust-version = "1.96" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 8559a69a..ba5244d3 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,19 +775,23 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **v2.1.10 "Fathom" ("Loom")**, the creator-tools-and-web-parity -step of the v2.1.5 → v2.2.0 "deepen the project" run — on the **creator** side it deepens -**TAStudio** (greenzone / force-greenzone caching so piano-roll seeking is instant, named -markers, richer branch slots) and broadens the **Lua API** (the full lifecycle-callback surface, -CPU/PPU/palette/OAM/CHR read-write with side-effect-free `*Debug` variants, HUD drawing, an -example-script library, and wasm-`piccolo` parity uplift); on the **web-parity** side it -completes the **browser RetroAchievements** path (wasm trampoline marshalling + a deployable -auth-proxy stack, ADR 0015) and brings **Vs. `DualSystem`** presentation to the **libretro** -core. The deterministic core is untouched — greenzone snapshots + Lua reads/writes preserve -the TAS/replay contract — so **AccuracyCoin stays 141/141 (100.00%)**, `visual_regression` is -byte-identical, and save-state / TAS / netplay replay stay bit-identical. (Vs. `DualSystem` on -the wasm present path is honestly deferred, and the browser-RA live login is a documented -deploy-to-verify step — see the CHANGELOG.) +RustyNES's current release is **v2.2.0 "Capstone"**, 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 (a browse-and-join room directory + server-side quick-play +over the existing room-code / TURN transport, delayed-stream spectators, a graded +hysteresis-based desync verdict, and multi-second peer-liveness RTT timeouts — all signaling +and telemetry only) and the **FDS medium model** (per-block **CRC-16/KERMIT** re-emitted on +every BIOS write + a synthesized gap/mark wire image, an opt-in continuous analog head-seek / +velocity model, and a BIOS-free synthetic write-verify oracle) — alongside a **peripherals** +pass (the Famicom `$4016`-bit-2 microphone and a 3×3-aperture Zapper light-timing model, both +default-off) and a **quality/security** pass (cargo-fuzz targets grown 3 → 8, which surfaced and +fixed two real OOM-DoS paths in the `.rnm` movie deserializer; a read-only ROM Info browser; +and four new MkDocs handbook pages). Every change is additive or default-off and the +deterministic core is untouched, so **AccuracyCoin holds 141/141 (100.00%)**, `visual_regression` +is byte-identical, `pal_apu_tests` is 10/10, and save-state / TAS / netplay replay stay +bit-identical. It follows **v2.1.10 "Fathom" ("Loom")**, the creator-tools-and-web-parity step +(TAStudio greenzone + Lua API breadth + browser RetroAchievements + Vs. `DualSystem` libretro +presentation). The v2.1.x line opened with **v2.1.0**, the accuracy-remediation release (a display-only PPU palette-backdrop-override fix, 86 mapper families promoted BestEffort → Curated, and @@ -913,7 +917,7 @@ If you use RustyNES in academic research, please cite: author = {RustyNES Contributors}, title = {RustyNES: A Cycle-Accurate NES Emulator in Rust}, year = {2026}, - version = {2.1.10}, + version = {2.2.0}, url = {https://github.com/doublegate/RustyNES}, note = {Cycle-accurate NES emulator on a master-clock-precise scheduler; AccuracyCoin 100\% (141/141), nestest 0-diff; 172 mapper families,