Skip to content

feat(fds,input): FDS medium model + Famicom mic + Zapper aperture (v2.2.0 Capstone)#291

Merged
doublegate merged 4 commits into
mainfrom
feat/v2.2.0-fds-peripherals
Jul 12, 2026
Merged

feat(fds,input): FDS medium model + Famicom mic + Zapper aperture (v2.2.0 Capstone)#291
doublegate merged 4 commits into
mainfrom
feat/v2.2.0-fds-peripherals

Conversation

@doublegate

Copy link
Copy Markdown
Owner

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 .fds runs +
the standard controller path) stays byte-identical and AccuracyCoin holds
141/141 (100%)
.

FDS medium model (F4.3, marquee) — crates/rustynes-mappers/src/fds.rs

  • Per-block CRC-16 on write. Reads present a synthesized byte-stream wire
    medium (lead-in / inter-block gaps, $80 start mark, block bytes, a
    CRC-16/KERMIT per block). 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.
  • 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. With the model off (the default), a non-writing .fds
    run is byte-identical.
  • Synthetic write-verify oracle (medium_write_verify). A BIOS-free walk
    of 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.rom and is exercised only from a
    gitignored tests/roms/external/ dump (documented in
    docs/accuracy-ledger.md).
  • Additive v4 save-state tail persists the head-seek state; v1/v2/v3 blobs
    load with the model disabled.

Remaining peripherals

  • Famicom microphone — modelled on $4016 bit 2 (Nes::set_microphone
    / Bus::set_microphone), wired through the frontend input path (hold-to-talk
    M key -> FrameInputs.microphone -> SharedInput -> latch). A $4016-only
    signal (never $4017); default-off leaves $4016 byte-identical. For
    Zelda (Pols Voice), Kid Icarus, etc.
  • Family BASIC positional keyboard — the full 9x8 matrix
    (FamilyKeyboardState + Subor clone) and its frontend key mapping were
    already modelled; verified complete, unchanged here.
  • Zapper light-timing — the photodiode now integrates a 3x3 aperture
    (field-of-view) around the aim point, asserting light only when >=2 pixels
    cross the luma threshold (ZAPPER_APERTURE_*), hardening detection vs the
    prior 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

  • AccuracyCoin 141/141 (100%); visual_regression / nestest /
    pal_apu_tests (20 total); save_state + battery_save (12); FDS mapper
    unit tests (69, incl. the new synthetic write-verify + head-seek + v4
    round-trip); input_devices harness (8); core+mappers (855); frontend lib
    (450, incl. the mic keybind test).
  • Gate green: cargo fmt --check; clippy --workspace --all-targets -D warnings + frontend scripting / scripting,hd-pack / retroachievements
    combos; RUSTDOCFLAGS=-D warnings cargo doc; markdownlint (pinned v0.39.0);
    no_std thumbv7em-none-eabihf cross-compile.

Docs

docs/mappers.md (FDS medium model), docs/accuracy-ledger.md (CI-verifiable
synthetic 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

Copilot AI review requested due to automatic review settings July 12, 2026 04:49
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.D2 end-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.

Comment thread crates/rustynes-mappers/src/fds.rs
Comment thread crates/rustynes-core/src/bus.rs
doublegate added a commit that referenced this pull request Jul 12, 2026
…+ 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>
@doublegate doublegate force-pushed the feat/v2.2.0-fds-peripherals branch from 1b5959c to 9dcdb3d Compare July 12, 2026 14:44
doublegate and others added 4 commits July 12, 2026 11:03
…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>
@doublegate doublegate force-pushed the feat/v2.2.0-fds-peripherals branch from 9dcdb3d to 51f2412 Compare July 12, 2026 15:06
@doublegate doublegate merged commit 08ce49d into main Jul 12, 2026
23 checks passed
@doublegate doublegate deleted the feat/v2.2.0-fds-peripherals branch July 12, 2026 15:24
doublegate added a commit that referenced this pull request Jul 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants