Skip to content

RustyNES v0.8.6

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Dec 06:41
· 13 commits to main since this release

RustyNES v0.8.6 - Sub-Cycle Accuracy Improvements (M11 S3-S5)

Hardware accuracy milestone - Implements DMC DMA cycle stealing, open bus behavior, and per-cycle mapper clocking. M11 83% complete.

Highlights

  • DMC DMA Cycle Stealing: CPU stalls 4 cycles per DMC sample fetch (matches hardware)
  • Open Bus Behavior: Tracks data bus state for unmapped memory reads
  • Controller Open Bus: Bits 5-7 properly mixed from open bus with controller data
  • Per-Cycle Mapper Clocking: Mapper IRQs now trigger at cycle-precise timing

What's Changed

DMC DMA Cycle Stealing

The DMC audio channel now properly steals CPU cycles when fetching samples:

// When DMC needs a sample, CPU stalls for 4 cycles
// This affects timing-sensitive code that runs during audio playback
bus.dmc_stall_cycles = 4;

Games that rely on precise timing during DMC playback will now behave correctly.

Open Bus Behavior

Implemented last_bus_value tracking for hardware-accurate unmapped memory reads:

// All memory operations now track the bus state
fn read(&mut self, addr: u16) -> u8 {
    let value = self.internal_read(addr);
    self.last_bus_value = value;  // Track for open bus
    value
}

fn write(&mut self, addr: u16, value: u8) {
    self.last_bus_value = value;  // Track for open bus
    self.internal_write(addr, value);
}

Reads from unmapped addresses return the last value on the data bus, matching NES hardware behavior.

Controller Open Bus

Controller reads now properly mix open bus bits with controller data:

// Controller returns: bits 0-4 from controller, bits 5-7 from open bus
let controller_bits = self.controller.read() & 0x1F;
let open_bus_bits = self.last_bus_value & 0xE0;
controller_bits | open_bus_bits

This fixes games that use bits 5-7 of controller port reads for copy protection or other purposes.

Per-Cycle Mapper Clocking

Mappers are now clocked on every CPU cycle via on_cpu_cycle():

fn on_cpu_cycle(&mut self) {
    // Clock mapper for cycle-accurate IRQ timing
    self.bus.mapper.clock(1);

    // Step PPU 3 dots
    self.bus.ppu.step();
    self.bus.ppu.step();
    self.bus.ppu.step();
}

This improves IRQ timing precision for mappers like MMC3 that use scanline counters.

Quality Metrics

  • Tests: 522+ passing (0 failures, 1 ignored doctest)
  • Blargg Pass Rate: 100% (90/90 tests)
  • New Tests: 2 open bus behavior tests added
  • M11 Progress: Sprints 3-5 complete (83%)

M11 Sub-Cycle Accuracy Milestone

Sprint Focus Status
S1 CPU cycle callback infrastructure Complete
S2 PPU synchronization per memory access Complete
S3 APU/DMC cycle stealing Complete
S4 Open bus behavior Complete
S5 Mapper cycle clocking Complete
S6 Final validation Pending

Technical Specifications

Feature Implementation
DMC DMA Stall 4 cycles per sample fetch
Open Bus Decay Instant (no capacitance simulation)
Controller Open Bus Bits 5-7 from last bus value
Mapper Clock 1 tick per CPU cycle

Installation

Pre-built Binaries

Download the appropriate binary for your platform from the Assets section below.

From Source

git clone https://github.com/doublegate/RustyNES.git
cd RustyNES
cargo build --release

Upgrading from v0.8.5

Recommended update - Significant hardware accuracy improvements. Drop-in replacement with no configuration changes required.


Full Changelog: v0.8.5...v0.8.6