Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/release-notes/v2.2.0.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RoomInfo> }` (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.
Expand Down
36 changes: 18 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</p>

<p align="center">
<a href="https://github.com/doublegate/RustyNES/actions"><img src="https://github.com/doublegate/RustyNES/workflows/CI/badge.svg" alt="Build Status"></a> <a href="#license"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg" alt="License: MIT OR Apache-2.0"></a> <a href="https://github.com/doublegate/RustyNES/releases"><img src="https://img.shields.io/badge/version-v2.1.10-blue.svg" alt="Version"></a> <a href="rust-toolchain.toml"><img src="https://img.shields.io/badge/rust-1.96-orange.svg" alt="Rust: 1.96"></a><br>
<a href="https://github.com/doublegate/RustyNES/actions"><img src="https://github.com/doublegate/RustyNES/workflows/CI/badge.svg" alt="Build Status"></a> <a href="#license"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg" alt="License: MIT OR Apache-2.0"></a> <a href="https://github.com/doublegate/RustyNES/releases"><img src="https://img.shields.io/badge/version-v2.2.0-blue.svg" alt="Version"></a> <a href="rust-toolchain.toml"><img src="https://img.shields.io/badge/rust-1.96-orange.svg" alt="Rust: 1.96"></a><br>
<a href="#compatibility-and-accuracy"><img src="https://img.shields.io/badge/AccuracyCoin-100%25%20(141%2F141)-brightgreen.svg" alt="AccuracyCoin"></a> <a href="#compatibility-and-accuracy"><img src="https://img.shields.io/badge/nestest-0--diff-brightgreen.svg" alt="nestest"></a> <a href="https://doublegate.github.io/RustyNES/"><img src="https://img.shields.io/badge/play-in%20browser-success.svg" alt="Try in browser"></a><br>
<a href="#platform-support"><img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Web%20%7C%20Android%20%7C%20iOS-lightgrey.svg" alt="Platform"></a>
</p>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading