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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ cycle-accurate core later replaced.
Dev / research tooling only — paths resolve repo-relative, and it touches no
crate and does not affect the build or the deterministic core.

- **`TakuikaNinja` FDS hardware-verification probes wired in (gated,
gitignored).** Added `crates/rustynes-test-harness/tests/fds_takuikaninja.rs`
with four `RUSTYNES_FDS_BIOS`-gated smoke tests against
`FDS-Mirroring-Test`, `FDS-4023-Test`, `FDS-Audio-Registers`, and
`FDS-4030D1-Addr` — real hardware-verified probes of `$4023`/mirroring/audio
register behavior and the FDS DRAM-refresh-watchdog IRQ. None of the four
carries an explicit permissive license, so they're staged gitignored under
`tests/roms/external/fds-takuikaninja/` (fetched from the author's GitHub
releases) rather than committed, mirroring the existing commercial-ROM
convention; every test skips cleanly when the BIOS or a probe disk is
absent, keeping CI clean by default. The underlying `$4023` and mirroring
behaviors these probes exercise are already implemented and unit-tested
independently in `crates/rustynes-mappers/src/fds.rs` — this is regression
insurance against a second, hardware-verified oracle, not a fix for a
gap. The
`$4030.D1` DRAM-watchdog probe tracks a known, honest residual (not yet
modeled by RustyNES or, per upstream, by most current FDS emulators) —
see `docs/accuracy-ledger.md`.

### Changed

- **Dependency consolidation (PR #305 — closes Dependabot #298–#303).** Rolled
Expand Down
133 changes: 133 additions & 0 deletions crates/rustynes-test-harness/tests/fds_takuikaninja.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! BIOS-gated smoke tests against `TakuikaNinja`'s FDS hardware-verification
//! probes (`FDS-Mirroring-Test`, `FDS-4023-Test`, `FDS-Audio-Registers`,
//! `FDS-4030D1-Addr` — see the `NESdev` Wiki "Emulator tests" page).
//!
//! None of these four ROMs carries an explicit permissive license (no
//! `LICENSE` file, nothing stated in any README as of this writing), so
//! unlike the rest of `tests/roms/` they are **not committed** — they live
//! gitignored under `tests/roms/external/fds-takuikaninja/`, the same
//! copyright-ambiguous staging convention used for commercial ROM dumps.
//! Every test here is a no-op skip (with a printed notice) unless both the
//! real FDS BIOS (`RUSTYNES_FDS_BIOS`, mirroring `fds.rs`) and the specific
//! probe disk are present on disk, so CI stays clean by default.
//!
//! These probe the exact `$4023`/mirroring behavior that
//! `crates/rustynes-mappers/src/fds.rs` already implements and unit-tests
//! independently (`clear_4023_stops_and_acks` et al.) — they are
//! regression-insurance against a second, hardware-verified oracle, not a
//! fix for a known gap. As with `fds_irq_tests_with_real_bios`, we assert
//! only that construction + a bounded run complete without panicking; these
//! ROMs render pass/fail state as on-screen text/register dumps, and
//! decoding that programmatically (framebuffer or nametable text-scrape) is
//! a follow-up once the exact screen layout is confirmed against real
//! hardware captures — asserting a specific outcome without that would
//! claim precision this harness does not actually verify.
//!
//! `FDS-4030D1-Addr` in particular probes the FDS DRAM-refresh-watchdog IRQ
//! (`$4030.D1`), which upstream research (and every current FDS emulator,
//! per the `NESdev` Wiki) explicitly notes as unimplemented/under-research;
//! `RustyNES` does not model it either, so that probe is expected to show
//! "not observed" (`XXXX`) rather than a specific timing value — this test
//! exists to track that honest residual, not to assert it away.
//!
//! Run all four (real BIOS + all four probe disks present):
//! ```text
//! RUSTYNES_FDS_BIOS=/path/to/disksys.rom \
//! cargo test -p rustynes-test-harness --features test-roms --test fds_takuikaninja -- --nocapture
//! ```

#![cfg(feature = "test-roms")]

use rustynes_core::Nes;

const BIOS_LEN: usize = 0x2000;

/// Shared skip-gated runner: reads the BIOS + the named probe disk from
/// `tests/roms/external/fds-takuikaninja/`, constructs, runs `frames`
/// frames, and prints a diagnostic. Returns early (with an `eprintln!` skip
/// notice) if either file is absent — the CI convention already established
/// by `fds_irq_tests_with_real_bios`.
fn run_probe(test_name: &str, rom_filename: &str, frames: u64) {
let Ok(bios_path) = std::env::var("RUSTYNES_FDS_BIOS") else {
eprintln!(
"SKIP {test_name}: set RUSTYNES_FDS_BIOS=/path/to/disksys.rom to run the \
TakuikaNinja FDS probes (BIOS is never committed)."
);
return;
};
let bios = match std::fs::read(&bios_path) {
Ok(b) if b.len() == BIOS_LEN => b,
Ok(b) => {
eprintln!(
"SKIP {test_name}: BIOS at {bios_path} is {} bytes, expected {BIOS_LEN}.",
b.len()
);
return;
}
Err(e) => {
eprintln!("SKIP {test_name}: cannot read {bios_path}: {e}");
return;
}
};

let disk_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/roms/external/fds-takuikaninja")
.join(rom_filename);
let disk = match std::fs::read(&disk_path) {
Ok(d) => d,
Err(e) => {
eprintln!(
"SKIP {test_name}: cannot read {}: {e} (fetch it from the TakuikaNinja \
GitHub release and place it there — see this file's module doc).",
disk_path.display()
);
return;
}
};

let mut nes = Nes::from_disk(&disk, &bios).expect("FDS construct with real BIOS");
for _ in 0..frames {
nes.run_frame();
}
eprintln!(
"{test_name}: ran {frames} frames with real BIOS ({rom_filename}). Mapper debug: {:?}",
nes.mapper_info()
);
}

/// `FDS-Mirroring-Test`: `$4025.D3` write, `$4030.D3` read, and the
/// previously-undocumented `$4023.D0=0` nametable-arrangement reset.
#[test]
fn fds_mirroring_test_with_real_bios() {
run_probe(
"fds_mirroring_test_with_real_bios",
"mirroring-test.fds",
600,
);
}

/// `FDS-4023-Test`: read-only register states ($4020-$409F) while toggling
/// bits 0/1 of `$4023`.
#[test]
fn fds_4023_test_with_real_bios() {
run_probe("fds_4023_test_with_real_bios", "4023-test.fds", 600);
}

/// `FDS-Audio-Registers`: FDS audio register readback while toggling
/// `$4023.D1` during wavetable playback.
#[test]
fn fds_audio_registers_with_real_bios() {
run_probe(
"fds_audio_registers_with_real_bios",
"audio-registers.fds",
600,
);
}

/// `FDS-4030D1-Addr`: DRAM-refresh-watchdog IRQ timing reported by
/// `$4030.D1`. Not modeled by `RustyNES` (nor, per upstream, by most current
/// FDS emulators) — this probe tracks that known, honest residual.
#[test]
fn fds_4030d1_addr_with_real_bios() {
run_probe("fds_4030d1_addr_with_real_bios", "4030d1-addr.fds", 600);
}
1 change: 1 addition & 0 deletions docs/accuracy-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ disposition under the v2.1.0 "Fathom" accuracy-remediation line
| NSF non-60 Hz playback + NSFe | **Done** (F4.1/F4.2): the play-speed divider (`$6E-$6F`/`$78-$79`) is parsed and a non-standard rate (PAL 50 Hz / custom µs) drives `play` via a mapper cycle-timer IRQ (frame-IRQ-disabled); standard 60 Hz keeps the byte-identical vblank-NMI path. `NSFE` chunked container parsed (INFO/DATA/BANK/auth). | `nsf` unit + core integration tests | **Done** (F4.1/F4.2) |
| FDS medium model (F4.3) | Byte-stream wire medium: gap / `$80` mark / block / **CRC-16-KERMIT** per block, with **per-block CRC re-emitted on write** (`resynth_block_crc`) and an opt-in **continuous belt-velocity head-seek** model (distance-proportional re-seek, default-off) replacing the fixed-cycle window | **CI-verifiable (synthetic):** `medium_write_verify` BIOS-free oracle — write via the register path, re-walk the wire, assert every block's CRC-16 + gap/mark framing round-trips (`fds::tests::synthetic_write_verify_*`). **Local-only:** the real-BIOS write-CRC path (BIOS recomputes CRC in its own RAM → `$4024`) needs a copyright `disksys.rom`, kept in gitignored `tests/roms/external/` and out of CI | **Shipped (v2.2.0 "Capstone")** — additive: default (model-off, non-writing) `.fds` run is **byte-identical**; new state round-trips the **v4** save-state tail. AccuracyCoin has no FDS ROM, so 141/141 is unaffected |
| Famicom microphone ($4016.2) | Not modeled | Built-in controller-2 mic bit surfaced on `$4016` D2 (`Nes::set_microphone`); `famicom_microphone_drives_4016_bit2` bus unit test | **Shipped (v2.2.0)** — additive / default-off (mic released ⇒ `$4016` byte-identical); a `$4016`-only signal (never touches `$4017`). No pass/fail mic ROM exists (real-cart local test only) |
| FDS DRAM-refresh-watchdog IRQ (`$4030.D1`) | Not modeled. The FDS BIOS/hardware raises periodic IRQs tied to DRAM-refresh-row-vs-access cycle accounting while `$4023.D0=0`; per upstream (`TakuikaNinja`'s `FDS-4030D1-Addr` research, `NESdev` Wiki) this is itself still under active hardware research and not modeled by most current FDS emulators either | `TakuikaNinja`'s `FDS-4030D1-Addr` probe (gitignored, `tests/roms/external/fds-takuikaninja/`, no permissive license — see `tests/roms/external/README.md`), consumed by the `RUSTYNES_FDS_BIOS`-gated `fds_4030d1_addr_with_real_bios` test | **Out of scope / honest residual, tracked not asserted** — the gated test only proves construction + a bounded real-BIOS run complete without panicking; it does not assert a specific watchdog timing value, since neither RustyNES nor the public research has pinned one yet. Revisit if/when upstream hardware research settles the exact behavior |
| Zapper light-timing | Single-pixel per-frame framebuffer sample | **Photodiode aperture** (3×3 field-of-view, ≥2 bright pixels — `ZAPPER_APERTURE_*`) vs the PPU per-dot output; `zapper_light_detected_for_bright_region` / `zapper_aperture_rejects_lone_bright_pixel` unit tests | **Hardened (v2.2.0)** — deterministic (pure fn of framebuffer + aim), no save-state change. No redistributable pass/fail Zapper ROM exists; the temporal ~19-26-scanline hold is finer than per-frame sampling (supported titles re-poll every frame) — full per-dot temporal integration is a documented future refinement (`docs/frontend.md`) |
| BestEffort mapper tier (26 families, was 112) | Register-decode + save-state round-trip only; off the oracle gate | `mapper_tier_honesty.rs` invariant | **Mostly remediated** (F3): 86 promoted to Curated with commercial-ROM oracle; the 26 left have no cleanly-booting dump (16 NES 2.0 high-id + 8 no-cart + 2 jam-at-boot) |
| MMC3 R1/R2 scanline-IRQ (ADR 0002) | ≤1-CPU-cycle differential on 4 `#[ignore]`'d sub-tests; zero game impact | `mmc3_test_2/4` #3 + siblings; `mmc3_r1r2_phase_probe` A12-phase golden probe (v2.1.5, `--features mmc3-a12-phase-probe`) | **CLOSED for the shipping default; axis-B candidate deferred to maintainer** (F5.0, ADR 0002). v2.1.5 direct instrumentation refined the closure: "no post-access qualifying rise" is ROM-specific (holds for the two `scanline_timing` #3 residuals, `irq_post=0`; **false** for `mmc3_test_v1/5`+`/6` #2, `irq_post=4` — post-access IRQ-clocking rises Session B never measured). Every *tested* lever stays non-curative (incl. the `mmc3-m2-phase-irq` deferral, byte-identical status on `/5`+`/6`); the four pins stay `#[ignore]`'d. One untested lever — an ares-style M2-edge-precise falling-edge low-time filter — is deferred to a maintainer decision (needs a sacred-gate-risking substrate change to prototype) |
Expand Down