feat(fds,input): FDS medium model + Famicom mic + Zapper aperture (v2.2.0 Capstone)#291
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
Pull request overview
This PR implements the v2.2.0 “Capstone” FDS medium-model capstone (wire-image synthesis with per-block CRC re-emission on write, plus an opt-in analog head-seek model) and completes the remaining input peripherals (Famicom microphone + Zapper aperture-based light detection), with corresponding docs/changelog updates. The changes are intended to remain additive/default-off so existing default execution stays byte-identical.
Changes:
- FDS medium-model completion: synthesized wire image with per-block CRC-16/KERMIT, CRC re-emitted on write, synthetic write-verify oracle, and additive v4 save-state tail for analog head-seek state.
- Input additions/hardening: Famicom microphone surfaced on
$4016.D2end-to-end (frontend → core), and Zapper light detection upgraded to a 3×3 aperture with a minimum-bright-pixel threshold. - Documentation updates across mapper docs, frontend docs, the accuracy ledger, and the changelog.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/mappers.md | Adds documentation for the new FDS medium model behavior and verification approach. |
| docs/frontend.md | Documents the new “Capstone” peripherals (mic, keyboard note, Zapper aperture sampling). |
| docs/accuracy-ledger.md | Updates the ledger entries to reflect the shipped FDS medium model + mic + Zapper changes and their verification split. |
| crates/rustynes-mappers/src/fds.rs | Implements CRC re-synthesis on writes, opt-in analog head-seek timing, synthetic medium verification, and bumps FDS save-state tail to v4. |
| crates/rustynes-frontend/src/input.rs | Tracks a hold-to-talk mic key and exposes it via InputState. |
| crates/rustynes-frontend/src/emu.rs | Adds FrameInputs.microphone and forwards it to Nes::set_microphone. |
| crates/rustynes-frontend/src/emu_thread.rs | Extends SharedInput to carry the mic state across threads. |
| crates/rustynes-frontend/src/app.rs | Wires mic state into FrameInputs on native builds. |
| crates/rustynes-core/src/nes.rs | Adds Nes::set_microphone API. |
| crates/rustynes-core/src/input_device.rs | Implements Zapper 3×3 aperture light sampling + tests. |
| crates/rustynes-core/src/bus.rs | Adds mic state to bus and ORs it into $4016 reads + tests. |
| CHANGELOG.md | Documents the user-visible v2.2.0 Capstone features. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…+ FDS tail offset invariant Two Copilot review threads on the v2.2.0 "Capstone" FDS + peripherals PR, both valid, both fixed with default-off byte-identity preserved. 1. bus.rs — DMC-DMA register-conflict read of $4016 dropped the microphone bit. The normal $4016 read path composes D2 from `famicom_mic` (`<< 2`), but `dmc_dma_read`'s `conflict_addr == 0x4016` branch rebuilt the byte from `controllers[0].read()` alone, so a DMC-DMA that collides with a $4016 read while the mic is asserted would have incorrectly cleared bit 2. Now OR in the same `mic = u8::from(self.famicom_mic) << 2` term, keeping the conflict path consistent with the normal controller-read semantics. Mic is default -off (released ⇒ mic == 0), so the returned byte is byte-identical to prior releases on the default; only an actively-held Famicom mic during a DMC/OAM DMA overlap is affected — the accurate behavior. 2. fds.rs — `load_disk_tail` read the v4 `pre_rewind_head` u32 out of `data[off..off+4]` without advancing `off`, breaking the "offset always reflects consumed bytes" invariant (functionally inert today since `off` was dead afterward, but a hazard for any future tail extension). Advance `off += 4` after the read, and add a `debug_assert_eq!(off, data.len())` that both documents/enforces the invariant (the top-of-fn length check already proved `expected == data.len()`, and both the v4 and v3 paths consume exactly to the blob end) and reads `off` on every path, so the new final assignment does not trip `unused_assignments` under `-D warnings`. Gate: fmt/clippy (workspace + frontend scripting/hd-pack/retroachievements)/ no_std thumbv7em/rustdoc/markdownlint all green; AccuracyCoin 141/141 (100%), visual_regression + nestest + pal_apu_tests byte-identical, FDS synthetic write-verify + NES-level save-state round-trip pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1b5959c to
9dcdb3d
Compare
…perture Emulation capstone (v2.2.0). FDS medium model: continuous belt-velocity head-seek model (opt-in, default-off, byte-identical fixed-window default), per-block CRC-16 re-emission on write, a BIOS-free synthetic write-verify oracle (medium_write_verify), and an additive v4 save-state tail. Peripherals: Famicom controller-2 microphone modeled on $4016 bit 2 (default-off, byte-identical), and a 3x3 photodiode-aperture Zapper light-sample replacing the single-pixel model (deterministic, no save-state change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…apstone) Thread the Famicom mic (M key) through InputState -> FrameInputs -> SharedInput -> EmuCore latch (Nes::set_microphone). Docs: mappers.md FDS medium model, accuracy-ledger.md CI-verifiable-vs-local-only split, frontend.md peripherals, CHANGELOG [Unreleased]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert doc links to pub(crate)/private items (FdsMediumError, reseek_window_cycles, ZAPPER_APERTURE_*) to plain code spans so the default cargo doc build passes -D warnings (project convention for feature-/visibility-limited item names).
…+ FDS tail offset invariant Two Copilot review threads on the v2.2.0 "Capstone" FDS + peripherals PR, both valid, both fixed with default-off byte-identity preserved. 1. bus.rs — DMC-DMA register-conflict read of $4016 dropped the microphone bit. The normal $4016 read path composes D2 from `famicom_mic` (`<< 2`), but `dmc_dma_read`'s `conflict_addr == 0x4016` branch rebuilt the byte from `controllers[0].read()` alone, so a DMC-DMA that collides with a $4016 read while the mic is asserted would have incorrectly cleared bit 2. Now OR in the same `mic = u8::from(self.famicom_mic) << 2` term, keeping the conflict path consistent with the normal controller-read semantics. Mic is default -off (released ⇒ mic == 0), so the returned byte is byte-identical to prior releases on the default; only an actively-held Famicom mic during a DMC/OAM DMA overlap is affected — the accurate behavior. 2. fds.rs — `load_disk_tail` read the v4 `pre_rewind_head` u32 out of `data[off..off+4]` without advancing `off`, breaking the "offset always reflects consumed bytes" invariant (functionally inert today since `off` was dead afterward, but a hazard for any future tail extension). Advance `off += 4` after the read, and add a `debug_assert_eq!(off, data.len())` that both documents/enforces the invariant (the top-of-fn length check already proved `expected == data.len()`, and both the v4 and v3 paths consume exactly to the blob end) and reads `off` on every path, so the new final assignment does not trip `unused_assignments` under `-D warnings`. Gate: fmt/clippy (workspace + frontend scripting/hd-pack/retroachievements)/ no_std thumbv7em/rustdoc/markdownlint all green; AccuracyCoin 141/141 (100%), visual_regression + nestest + pal_apu_tests byte-identical, FDS synthetic write-verify + NES-level save-state round-trip pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9dcdb3d to
51f2412
Compare
…ripherals + quality/security) (#297) The milestone cut that closes the v2.1.5 -> v2.2.0 "deepen the existing project" run, landing its two remaining marquees plus a peripherals and quality/security pass. Every change already merged (PRs #290, #291) is additive or default-off; the deterministic core is untouched, so this cut itself moves only version strings + release docs and no framebuffer/audio byte. Marquees (already on main via #290/#291): - Netplay matchmaking + lobby (B5) — a browse-and-join room directory (`ListRooms`/`RoomList`) and server-side quick-play (`QuickMatch`/`Matched`) over the existing room-code / TURN transport, delayed-stream spectators (`SpectatorConfig.delay_frames`, reveal-horizon gated), a graded hysteresis desync verdict (`DesyncStatus`, threshold 3 ~= 1.5 s), and graded multi-second peer-liveness RTT timeouts (`PeerLink` + `DisconnectReason::PeerTimeout`). Signaling and telemetry only — the rollback/determinism contract is untouched. - FDS medium model completion (F4.3) — per-block CRC-16/KERMIT re-emitted on every BIOS write (`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` window, 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 — copyright disksys.rom). Additive v4 FDS save-state tail; with the model off a non-writing `.fds` run is byte-identical to prior releases. Peripherals (#291): the Famicom `$4016`-bit-2 microphone (`Nes::set_microphone`, `$4016`-only, never `$4017`) and a 3x3-aperture Zapper light-timing model (`ZAPPER_APERTURE_*`), both additive/default-off. Quality/security (#290): cargo-fuzz targets grown 3 -> 8 (`ppu_reg_io`, `apu_reg_io`, the high-value `netplay_message`, `save_state`, `movie`), which surfaced and fixed two real OOM-DoS paths in `Movie::deserialize` (untrusted `frame_count` -> unbounded `Vec::with_capacity`, and a zero-`bytes_per_frame` infinite push loop) byte-identically for valid input; a read-only Tools -> ROM Info browser (dump-identity CRC32 keys + SHA-256 + per-game DB entry + decoded header, `&Nes`, never mutates); and four new MkDocs handbook pages. Cut mechanics: `[workspace.package] version` 2.1.10 -> 2.2.0 (Cargo.lock refreshed across all 18 workspace crates), CHANGELOG `[Unreleased]` renamed to `[2.2.0]` with a fresh empty `[Unreleased]`, `.github/release-notes/ v2.2.0.md` authored, and README badge / BibTeX / current-release paragraph updated. `release-auto.yml` tags + publishes on main going green. Sacred gate (verified on the merged #290+#291 tree): AccuracyCoin 141/141 (100.00%), nestest 0-diff, `pal_apu_tests` 10/10, `visual_regression` byte-identical, netplay rollback-determinism + snapshot-restore-replay + FDS synthetic write-verify green, `no_std` thumbv7em cross-compile, full host fmt/clippy/rustdoc/markdownlint, version consistency. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
The emulation capstone for RustyNES v2.2.0 "Capstone": the FDS medium
model (F4.3, the marquee) plus the remaining input peripherals. All changes are
additive and default-off, so the shipped default (non-writing
.fdsruns +the standard controller path) stays byte-identical and AccuracyCoin holds
141/141 (100%).
FDS medium model (F4.3, marquee) —
crates/rustynes-mappers/src/fds.rsmedium (lead-in / inter-block gaps,
$80start mark, block bytes, aCRC-16/KERMIT per block). Each BIOS-written block now re-emits a fresh
per-block CRC-16 over its updated payload (
resynth_block_crc), modellingthe RP2C33 controller's continuous CRC generator so the medium stays
self-consistent after a write.
Fds::set_analog_head_seek). Replaces the flat fixedHEAD_RESEEK_CYCLESmotor-restart not-ready window with a belt-driven, distance-proportional
seek time (
HEAD_SEEK_BYTES_PER_CYCLEvelocity +HEAD_SEEK_SETTLE_CYCLESsettle, clamped to a cold spin-up), sized from the head-travel distance
captured at motor-off. With the model off (the default), a non-writing
.fdsrun is byte-identical.
medium_write_verify). A BIOS-free walkof the wire image asserting 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.romand is exercised only from agitignored
tests/roms/external/dump (documented indocs/accuracy-ledger.md).load with the model disabled.
Remaining peripherals
$4016bit 2 (Nes::set_microphone/
Bus::set_microphone), wired through the frontend input path (hold-to-talkMkey ->FrameInputs.microphone->SharedInput-> latch). A$4016-onlysignal (never
$4017); default-off leaves$4016byte-identical. ForZelda (Pols Voice), Kid Icarus, etc.
9x8matrix(
FamilyKeyboardState+ Subor clone) and its frontend key mapping werealready modelled; verified complete, unchanged here.
(field-of-view) around the aim point, asserting light only when >=2 pixels
cross the luma threshold (
ZAPPER_APERTURE_*), hardening detection vs theprior single-pixel sample. Deterministic (pure function of the presented
framebuffer), no save-state change. The finer ~19-26-scanline temporal hold is
below per-frame sample resolution (supported titles re-poll every frame); full
per-dot temporal integration is a documented future refinement.
Verification
visual_regression/nestest/pal_apu_tests(20 total);save_state+battery_save(12); FDS mapperunit tests (69, incl. the new synthetic write-verify + head-seek + v4
round-trip);
input_devicesharness (8); core+mappers (855); frontend lib(450, incl. the mic keybind test).
cargo fmt --check;clippy --workspace --all-targets -D warnings+ frontendscripting/scripting,hd-pack/retroachievementscombos;
RUSTDOCFLAGS=-D warnings cargo doc; markdownlint (pinned v0.39.0);no_stdthumbv7em-none-eabihfcross-compile.Docs
docs/mappers.md(FDS medium model),docs/accuracy-ledger.md(CI-verifiablesynthetic vs local-only real-BIOS),
docs/frontend.md(Capstone peripherals),CHANGELOG.md[Unreleased].Do NOT merge — the maintainer runs the release ceremony.
🤖 Generated with Claude Code