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
36 changes: 0 additions & 36 deletions crates/rustynes-apu/src/apu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ pub struct Apu {
/// is suppressed and this decrements; at 0 the arm fires. Default 0.
// (W3-Stage-3: also dead under `mc-r1-dmc-delayed-4015`, which supersedes
// the halt-subpos pre-arm with the emergent consume-edge arm.)
#[allow(dead_code)]
pub(crate) subpos_arm_countdown: u8,
/// Latches the one-byte looping edge where the next reload request is
/// lost because it is raised too soon after a DMA get. Cleared when a
Expand Down Expand Up @@ -326,14 +325,6 @@ pub const CHANNEL_MASK_ALL: u8 = 0x3F;
pub const CHANNEL_GAIN_UNITY: [f32; 6] = [1.0; 6];

impl Apu {
const fn dmc_abort_delay_for(cycles_until_output: u16) -> Option<u8> {
match cycles_until_output {
2 => Some(2),
3 => Some(3),
_ => None,
}
}

/// New APU.
#[must_use]
pub fn new(region: Region, sample_rate: u32) -> Self {
Expand Down Expand Up @@ -1939,33 +1930,6 @@ impl Apu {
}
}

// W3-Stage-3: under `mc-r1-dmc-delayed-4015` the explicit abort is
// emergent from the delayed-status service gate, so this floor scheduling
// compensation has no caller (superseded, retained for the flag-off path).
#[allow(dead_code)]
fn schedule_explicit_dmc_abort_if_needed(&mut self) {
if self.dmc.bits_remaining != 1 || self.dmc.sample_buffer.is_none() {
return;
}
// `first_apu_clock` is "CPU cycles until the next APU (DMC) clock". The
// DMC clocks on `apu_phase`-true, so `apu_phase ? 2 : 1` is correct.
let first_apu_clock = if self.apu_phase { 2 } else { 1 };
let cycles_until_output = first_apu_clock + self.dmc.timer.saturating_mul(2);
if cycles_until_output == 1 {
self.pending_dmc_dma = true;
self.dmc_dma_is_load = false;
self.dmc_dma_short = false;
self.dmc_dma_addr = self.dmc.dma_addr();
self.dmc_need_halt = true;
self.dmc_need_dummy_read = true;
self.pending_dmc_abort = true;
return;
}
if let Some(delay) = Self::dmc_abort_delay_for(cycles_until_output) {
self.dmc_abort_delay = delay;
}
}

/// CPU register read (only `$4015` is meaningful). Reading clears the
/// frame IRQ flag.
pub fn read_status(&mut self) -> u8 {
Expand Down
202 changes: 0 additions & 202 deletions crates/rustynes-core/src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,6 @@ pub struct LockstepBus {
/// APU instance.
pub(crate) apu: Apu,
/// Cartridge metadata (kept for save-state and debugger).
#[allow(dead_code)]
pub(crate) cart: Cartridge,
/// Boxed mapper.
pub(crate) mapper: Box<dyn Mapper>,
Expand Down Expand Up @@ -3219,76 +3218,6 @@ impl LockstepBus {
self.last_nmi_level = level;
}

/// Drain any cycles owed to the DMA controllers (OAM DMA + DMC DMA)
/// before completing a CPU access.
///
/// Called from `cpu_read` and `cpu_write`. DMC DMA preempts OAM DMA per
/// nesdev: while OAM DMA is running and a DMC DMA also fires, the DMC
/// fetch happens "between the dummy and DMA reads" of the OAM DMA,
/// stalling the OAM transfer for 3-4 extra cycles. Our simpler model
/// services DMC DMA at the start of each cycle the bus controls; if
/// OAM DMA is in flight, the DMC DMA inserts itself between transfer
/// pairs.
// Under `mc-r1-full-cpu` the body reduces to the DMC-abort/idle handling;
// OAM moved to the CPU-driven `oam_dma_step`, so `&mut self`/`read_addr` are
// only lightly used here — silence the resulting lints under the flag.
#[allow(
clippy::unused_self,
clippy::needless_pass_by_ref_mut,
clippy::missing_const_for_fn
)]
fn drain_dma(&mut self, read_addr: Option<u16>) {
// Stage-D: under `mc-r1-full-cpu` the OAM DMA is CPU-driven (read1), so
// the legacy OAM block below (the only user of `read_addr` once the
// abort-cancel path owns aborts) is cfg'd out — silence the param.
let _ = read_addr;
// Sprint 3 iter 3 — under the `dmc-get-put-scheduler`
// feature, the abort is handled INSIDE `service_dmc_dma` /
// `service_dmc_dma_during_oam` (matching Mesen2's unified
// `RunDma` loop where the `processCycle` lambda checks
// `_abortDmcDma` per iteration). The pre-service abort
// call is preserved on the default-off path.
// accuracycoin-100 Phase 2: under `mc-r1-dmc-abort-cancel` the R1
// read1/write1 path OWNS the abort (get-cycle 1-halt Y=1 / put-write
// cancel Y=0). Skip the legacy `drain_dma` service — `drain_dma(None)`
// runs every R1 `cpu_clock` cycle and would `complete_dmc_abort` the
// pending abort BEFORE the read1 hook can see it (the inert-as-placed
// bug). The legacy service below stays active for the default build.
// Stage-D (`mc-r1-full-cpu`): OAM DMA is CPU-driven in `read1`
// (`oam_dma_step`), NOT bus-side burst — leave `dma_pending` set for the
// read1 loop to consume; drain_dma does no OAM work under the flag.
}

fn clock_oam_dma_cycle(&mut self, total: u32, alignment: u32) {
let consumed = total - self.dma_cycles_owed; // 0, 1, ...
if consumed < alignment {
// OAM DMA halt / alignment cycle — bus idle for the CPU,
// but the DMA controller owns it. Trace as DmaRead with
// the halted CPU read address so the trace shows what the
// open-bus latch ended up driving (Session-21).
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, self.dma_halt_addr, self.open_bus);
self.tick_one_cpu_cycle();
self.dma_cycles_owed -= 1;
return;
}
let xfer_idx = consumed - alignment; // 0..512
// Even xfer index: read; odd: write.
if xfer_idx & 1 == 0 {
let src_addr =
(u16::from(self.dma_page) << 8) | u16::try_from(xfer_idx >> 1).unwrap_or(0);
self.dma_byte = self.raw_oam_dma_read(src_addr);
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, src_addr, self.dma_byte);
} else {
self.oam_dma_put();
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaWrite, 0x2004, self.dma_byte);
}
self.tick_one_cpu_cycle();
self.dma_cycles_owed -= 1;
}

/// OAM-DMA source fetch (Session-26 / Sprint 2 iter 4).
///
/// The 2A03 has three internal address buses (6502, OAM DMA, DMC
Expand Down Expand Up @@ -3464,133 +3393,6 @@ impl LockstepBus {
self.trace_bus_data = data;
}

/// Service one DMC DMA transfer.
///
/// Per nesdev §DMC DMA:
/// - Halt cycle (1 CPU cycle).
/// - Dummy cycle (1 CPU cycle).
/// - Optional alignment cycle if the DMA get would otherwise land on
/// a put cycle.
/// - One memory-read/get cycle.
///
/// While CPU is halted, the previous read is logically repeated. For
/// `$4015` / `$4016` / `$4017` / `$2007` this has the documented
/// register-readout side-effect bug; PAL fixes it.
///
/// v1.2 Sprint 3.2 — two implementations now coexist via the
/// `dmc-get-put-scheduler` cargo feature (ADR-0007). With the
/// flag OFF, the v1.0/v1.1 baseline ("phase-agnostic noop loop +
/// compensating delays") is preserved bit-identically — the four
/// delays `dmc_dma_short`, `dmc_dma_cooldown`, `dmc_abort_delay`,
/// `dmc_dma_delay` are still load-bearing. With the flag ON, the
/// new path uses Mesen2's get/put cycle alternation model
/// (`NesCpu.cpp:399-450`) — `dmc_need_halt` and
/// `dmc_need_dummy_read` on the APU are consumed cycle-by-cycle,
/// and the four compensating delays are NO-OPS under the new
/// model. The new path closes the cycle-2 implied-dummy-read
/// cascade that 6 prior single-delay tweaks could not.
// Phase B: the DMC burst is CPU-driven-interleaved under R1, so this
// bus-side burst is unused there (still used on the default path).
#[allow(dead_code)]
fn service_dmc_dma(&mut self, halted_addr: u16) {
if !self.apu.dmc_dma_pending() || self.in_dmc_dma {
return;
}
let addr = self.apu.dmc_dma_addr();
let noop_cycles = if self.apu.dmc_dma_short() { 2 } else { 3 };
self.in_dmc_dma = true;
self.capture_deferred_dma_replay();

for _ in 0..noop_cycles {
self.replay_dma_noop_read(halted_addr);
// Session-21: tag DMC halt/dummy/align cycles as DmaRead with
// the halted CPU address (which is what the open-bus latch
// sees on real silicon during those cycles).
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
self.tick_one_cpu_cycle();
}

// Perform the actual sample read/get and deliver back to the APU.
let byte = self.dmc_dma_read(addr, halted_addr);
if self.apu.dmc_dma_deliver_before_tick() {
self.apu.complete_dmc_dma_before_get_tick(byte);
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
self.tick_one_cpu_cycle();
} else {
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
self.tick_one_cpu_cycle();
self.apu.complete_dmc_dma(byte);
}
self.in_dmc_dma = false;
}

#[allow(dead_code)]
fn service_dmc_abort(&mut self, halted_addr: u16) {
if !self.apu.dmc_abort_pending() || self.in_dmc_dma {
return;
}
self.in_dmc_dma = true;
self.replay_dma_noop_read(halted_addr);
// Session-21: abort halt cycle is observable from the bus as a
// DmaRead of the halted CPU address (open-bus driver retained).
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
self.tick_one_cpu_cycle();
self.apu.complete_dmc_abort();
self.in_dmc_dma = false;
}

#[allow(dead_code)]
fn service_dmc_dma_during_oam(&mut self, total: u32, alignment: u32) {
if !self.apu.dmc_dma_pending() || self.in_dmc_dma {
return;
}
let addr = self.apu.dmc_dma_addr();
let noop_cycles = if self.apu.dmc_dma_short() { 2 } else { 3 };
let halted_addr = self.dma_halt_addr;
self.in_dmc_dma = true;
self.capture_deferred_dma_replay();

// DMC halt, dummy, and alignment no-op cycles overlap with OAM DMA.
// The 6502 core remains halted, but OAM can keep consuming its own
// read/write slots on those cycles.
for _ in 0..noop_cycles {
self.replay_dma_noop_read(halted_addr);
if self.dma_cycles_owed > 0 {
// clock_oam_dma_cycle owns its own trace tagging.
self.clock_oam_dma_cycle(total, alignment);
} else {
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
self.tick_one_cpu_cycle();
}
}

// The actual DMC get owns the memory read cycle. If OAM still has a
// transfer pending, this skips one OAM slot and forces the next OAM
// read to realign on a later get cycle.
let byte = self.dmc_dma_read(addr, halted_addr);
let deliver_before_tick = self.apu.dmc_dma_deliver_before_tick();
if deliver_before_tick {
self.apu.complete_dmc_dma_before_get_tick(byte);
}
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
self.tick_one_cpu_cycle();
if self.dma_cycles_owed > 0 {
#[cfg(feature = "irq-timing-trace")]
self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
self.tick_one_cpu_cycle();
}
if !deliver_before_tick {
self.apu.complete_dmc_dma(byte);
}
self.in_dmc_dma = false;
}

const fn capture_deferred_dma_replay(&mut self) {
self.deferred_dma_replay_addr = match self.open_bus {
0x02 => 0x2002,
Expand Down Expand Up @@ -4249,8 +4051,6 @@ impl LockstepBus {

impl Bus for LockstepBus {
fn cpu_read(&mut self, addr: u16) -> u8 {
// Drain any pending DMA before doing the requested access.
self.drain_dma(Some(addr));
if self.deferred_dma_replay_addr != 0
&& self.open_bus == (self.deferred_dma_replay_addr >> 8) as u8
{
Expand Down Expand Up @@ -4318,7 +4118,6 @@ impl Bus for LockstepBus {
}

fn cpu_write(&mut self, addr: u16, value: u8) {
self.drain_dma(None);
self.open_bus = value;
// Mirror the CPU-initiated write onto the internal data bus.
// Symmetric with `raw_cpu_read`'s mirror — DMC DMA does not
Expand Down Expand Up @@ -4716,7 +4515,6 @@ impl Bus for LockstepBus {
self.commit_controller_strobe(value);
}
}
self.drain_dma(None);
self.cycle = self.cycle.wrapping_add(1);
self.ppu.on_cpu_cycle();
// v2.8.0 Phase 4 — skip the virtual dispatch on boards whose
Expand Down
8 changes: 0 additions & 8 deletions crates/rustynes-frontend/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ impl SampleQueue {

/// v1.0.0 — the current master output gain.
#[must_use]
#[allow(dead_code)] // read in tests + as a UI mirror.
pub fn gain(&self) -> f32 {
#[allow(clippy::cast_possible_truncation)] // low 32 bits hold the f32.
f32::from_bits(self.inner.gain.load(Ordering::Relaxed) as u32)
Expand Down Expand Up @@ -448,29 +447,25 @@ impl SampleQueue {
}

/// Number of buffered samples (racy snapshot; informational).
#[allow(dead_code)] // Used by tests + the Performance panel.
pub fn len(&self) -> usize {
let tail = self.inner.tail.load(Ordering::Acquire);
let head = self.inner.head.load(Ordering::Acquire);
tail.wrapping_sub(head)
}

/// True if the queue is empty.
#[allow(dead_code)] // Used by tests + the Performance panel.
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Total samples dropped at the ring cap / by hard resync so far
/// (overruns). v2.8.0 health counter for the Performance panel.
#[allow(dead_code)] // wasm builds render the panel without native audio.
pub fn overrun_dropped(&self) -> u64 {
self.inner.overrun_dropped.load(Ordering::Relaxed)
}

/// Total short callback fills (underruns) so far. v2.8.0 health counter
/// for the Performance panel.
#[allow(dead_code)] // wasm builds render the panel without native audio.
pub fn underruns(&self) -> u64 {
self.inner.underruns.load(Ordering::Relaxed)
}
Expand Down Expand Up @@ -596,7 +591,6 @@ pub struct AudioOutput {
pub sample_rate: u32,
/// Number of channels (we render mono, but duplicate to fill stereo).
/// Informational; the duplication happens inside the audio callback.
#[allow(dead_code)]
pub channels: u16,
/// Producer-side queue handle (push from the emulator thread).
pub queue: SampleQueue,
Expand All @@ -619,7 +613,6 @@ pub struct AudioOutput {
impl AudioOutput {
/// Open the default output device with the pre-v2.8.0 defaults (device
/// default rate, 60 ms latency target, DRC on). Kept for tests.
#[allow(dead_code)]
pub fn try_default() -> Result<Self, AudioError> {
Self::try_new(None, 60, true, None)
}
Expand Down Expand Up @@ -830,7 +823,6 @@ impl AudioOutput {
}

/// The latency target in samples (Performance panel readout).
#[allow(dead_code)]
#[must_use]
pub const fn latency_target_samples(&self) -> usize {
self.latency_samples
Expand Down
2 changes: 0 additions & 2 deletions crates/rustynes-frontend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2104,7 +2104,6 @@ impl Config {
/// # Errors
///
/// Returns [`ConfigError`] on I/O or serialization failure.
#[allow(dead_code)]
pub fn save(&self) -> Result<(), ConfigError> {
let Some(path) = Self::default_path() else {
return Ok(());
Expand All @@ -2117,7 +2116,6 @@ impl Config {
/// # Errors
///
/// Returns [`ConfigError`] on I/O or serialization failure.
#[allow(dead_code)]
pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
Expand Down
2 changes: 0 additions & 2 deletions crates/rustynes-frontend/src/debugger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,14 +834,12 @@ pub struct DebuggerOverlay {
/// v1.7.0 "Forge" beta.5 (#55) — the toolbar HUD that displayed this was
/// removed; the status bar shows FPS from its own `ShellFrame`. The setter
/// + field are retained for the stable public API (and future panels).
#[allow(dead_code)]
fps: f32,
/// v1.4.0 Sprint 4.2 — current TAS movie record/playback status, pushed by
/// [`DebuggerOverlay::set_movie_status`] from the pacing loop. v1.7.0
/// "Forge" beta.5 (#55) — retained for the public API after the toolbar HUD
/// that displayed it was removed (movie state is now in the menu + status
/// bar).
#[allow(dead_code)]
movie: MovieStatus,
/// v2.7.1 — `RetroAchievements` badge-image cache (achievement icons in the
/// panel rows + unlock toasts). Lazily created the first time a badge URL is
Expand Down
Loading