Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
This commit is contained in:
314
src/native_core/bus/tests/apu.rs
Normal file
314
src/native_core/bus/tests/apu.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apu_frame_irq_asserts_in_4_step_mode() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||
|
||||
for _ in 0..14_918u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
|
||||
assert!(bus.poll_irq(), "APU frame IRQ should assert in 4-step mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_4015_clears_apu_frame_irq_flag() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||
|
||||
for _ in 0..14_918u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
|
||||
let status = bus.read(0x4015);
|
||||
assert_ne!(status & 0x40, 0, "frame IRQ bit should be set in status");
|
||||
assert!(!bus.poll_irq(), "reading 4015 should clear frame IRQ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apu_frame_irq_inhibit_bit_disables_irq_and_clears_pending() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||
for _ in 0..14_918u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert!(bus.poll_irq());
|
||||
|
||||
bus.write(0x4017, 0x40); // 4-step, IRQ inhibit
|
||||
assert!(
|
||||
!bus.poll_irq(),
|
||||
"inhibit write should clear pending frame IRQ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writing_4015_does_not_acknowledge_apu_frame_irq() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||
for _ in 0..14_918u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert!(bus.poll_irq(), "frame IRQ must be pending");
|
||||
|
||||
// Recreate pending frame IRQ and ensure $4015 write does not clear it.
|
||||
for _ in 0..14_918u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
bus.write(0x4015, 0x00);
|
||||
assert!(bus.poll_irq(), "writing $4015 must not clear frame IRQ");
|
||||
|
||||
// Reading $4015 still acknowledges frame IRQ as expected.
|
||||
let _ = bus.read(0x4015);
|
||||
assert!(!bus.poll_irq(), "reading $4015 should clear frame IRQ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apu_5step_mode_does_not_generate_frame_irq() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4017, 0x80); // 5-step mode
|
||||
|
||||
for _ in 0..20_000u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert!(!bus.poll_irq(), "5-step mode must not assert frame IRQ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apu_write_only_register_reads_return_cpu_open_bus() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4000, 0x12);
|
||||
bus.write(0x0000, 0xAB);
|
||||
assert_eq!(bus.read(0x0000), 0xAB);
|
||||
|
||||
assert_eq!(bus.read(0x4000), 0xAB);
|
||||
assert_eq!(bus.read(0x400E), 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writing_4017_in_5step_mode_clocks_half_frame_after_delay() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4015, 0x01); // enable pulse1
|
||||
bus.write(0x4000, 0x00); // length halt disabled
|
||||
bus.write(0x4003, 0x18); // length index 3 => 2
|
||||
assert_eq!(bus.apu.length_counters[0], 2);
|
||||
|
||||
bus.write(0x4017, 0x80); // switch to 5-step mode
|
||||
assert_eq!(bus.apu.length_counters[0], 2);
|
||||
for _ in 0..2u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.length_counters[0], 2);
|
||||
bus.clock_cpu(1); // reset delay complete (3 CPU cycles on even phase)
|
||||
assert_eq!(bus.apu.length_counters[0], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_roundtrip_preserves_apu_frame_counter_fields() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.apu.frame_cycle = 777;
|
||||
bus.apu.frame_mode_5step = true;
|
||||
bus.apu.frame_irq_inhibit = true;
|
||||
bus.apu.frame_irq_pending = true;
|
||||
bus.apu.channel_enable_mask = 0x1F;
|
||||
bus.apu.length_counters = [1, 2, 3, 4];
|
||||
bus.apu.dmc_bytes_remaining = 99;
|
||||
bus.apu.dmc_irq_enabled = true;
|
||||
bus.apu.dmc_irq_pending = true;
|
||||
bus.apu.dmc_cycle_counter = 1234;
|
||||
bus.apu.envelope_divider = [9, 8, 7];
|
||||
bus.apu.envelope_decay = [6, 5, 4];
|
||||
bus.apu.envelope_start_flags = 0x05;
|
||||
bus.apu.triangle_linear_counter = 3;
|
||||
bus.apu.triangle_linear_reload_flag = true;
|
||||
bus.apu.sweep_divider = [11, 12];
|
||||
bus.apu.sweep_reload_flags = 0x03;
|
||||
bus.apu.cpu_cycle_parity = true;
|
||||
bus.apu.frame_reset_pending = true;
|
||||
bus.apu.frame_reset_delay = 2;
|
||||
bus.apu.pending_frame_mode_5step = true;
|
||||
bus.apu.pending_frame_irq_inhibit = false;
|
||||
|
||||
let mut raw = Vec::new();
|
||||
bus.save_state(&mut raw);
|
||||
|
||||
let mut restored = NativeBus::new(Box::new(StubMapper));
|
||||
restored.load_state(&raw).expect("state should load");
|
||||
assert_eq!(restored.apu.frame_cycle, 777);
|
||||
assert!(restored.apu.frame_mode_5step);
|
||||
assert!(restored.apu.frame_irq_inhibit);
|
||||
assert!(restored.apu.frame_irq_pending);
|
||||
assert_eq!(restored.apu.channel_enable_mask, 0x1F);
|
||||
assert_eq!(restored.apu.length_counters, [1, 2, 3, 4]);
|
||||
assert_eq!(restored.apu.dmc_bytes_remaining, 99);
|
||||
assert!(restored.apu.dmc_irq_enabled);
|
||||
assert!(restored.apu.dmc_irq_pending);
|
||||
assert_eq!(restored.apu.dmc_cycle_counter, 1234);
|
||||
assert_eq!(restored.apu.envelope_divider, [9, 8, 7]);
|
||||
assert_eq!(restored.apu.envelope_decay, [6, 5, 4]);
|
||||
assert_eq!(restored.apu.envelope_start_flags, 0x05);
|
||||
assert_eq!(restored.apu.triangle_linear_counter, 3);
|
||||
assert!(restored.apu.triangle_linear_reload_flag);
|
||||
assert_eq!(restored.apu.sweep_divider, [11, 12]);
|
||||
assert_eq!(restored.apu.sweep_reload_flags, 0x03);
|
||||
assert!(restored.apu.cpu_cycle_parity);
|
||||
assert!(restored.apu.frame_reset_pending);
|
||||
assert_eq!(restored.apu.frame_reset_delay, 2);
|
||||
assert!(restored.apu.pending_frame_mode_5step);
|
||||
assert!(!restored.apu.pending_frame_irq_inhibit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apu_status_reflects_length_counters_and_disable_clears_them() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4015, 0x0F); // enable pulse1/pulse2/triangle/noise
|
||||
bus.write(0x4003, 0xF8); // load pulse1 length index 31
|
||||
bus.write(0x4007, 0xF8); // load pulse2
|
||||
bus.write(0x400B, 0xF8); // load triangle
|
||||
bus.write(0x400F, 0xF8); // load noise
|
||||
|
||||
let status = bus.read(0x4015);
|
||||
assert_eq!(status & 0x0F, 0x0F);
|
||||
|
||||
bus.write(0x4015, 0x00);
|
||||
let status2 = bus.read(0x4015);
|
||||
assert_eq!(status2 & 0x0F, 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apu_length_counter_decrements_on_half_frame_when_not_halted() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4015, 0x01); // enable pulse1
|
||||
bus.write(0x4000, 0x00); // halt=0
|
||||
bus.write(0x4003, 0x18); // length index 3 => value 2
|
||||
|
||||
assert_eq!(bus.apu.length_counters[0], 2);
|
||||
for _ in 0..7_457u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.length_counters[0], 1);
|
||||
for _ in 0..7_458u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.length_counters[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmc_irq_raises_and_is_reported_in_4015_status() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4010, 0x8F); // IRQ enable, no loop, fastest rate
|
||||
bus.write(0x4013, 0x00); // sample length = 1 byte
|
||||
bus.write(0x4015, 0x10); // enable DMC
|
||||
|
||||
for _ in 0..54u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert!(bus.poll_irq());
|
||||
|
||||
let status = bus.read(0x4015);
|
||||
assert_ne!(status & 0x80, 0, "DMC IRQ should be visible in status");
|
||||
assert!(bus.poll_irq(), "status read must not clear DMC IRQ");
|
||||
bus.write(0x4015, 0x10);
|
||||
assert!(!bus.poll_irq(), "writing 4015 acknowledges DMC IRQ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quarter_frame_clocks_triangle_linear_counter() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4008, 0x05); // control=0, reload value=5
|
||||
bus.write(0x400B, 0x00); // set reload flag
|
||||
|
||||
for _ in 0..3_729u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.triangle_linear_counter, 5);
|
||||
assert!(!bus.apu.triangle_linear_reload_flag);
|
||||
|
||||
for _ in 0..3_728u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.triangle_linear_counter, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quarter_frame_envelope_start_reloads_decay() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4015, 0x01); // enable pulse1
|
||||
bus.write(0x4000, 0x03); // envelope period=3
|
||||
bus.write(0x4003, 0x00); // start envelope
|
||||
assert_ne!(bus.apu.envelope_start_flags & 0x01, 0);
|
||||
|
||||
for _ in 0..3_729u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.envelope_decay[0], 15);
|
||||
assert_eq!(bus.apu.envelope_divider[0], 3);
|
||||
assert_eq!(bus.apu.envelope_start_flags & 0x01, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_half_frame_updates_pulse_timer_period() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4002, 0x00); // timer low
|
||||
bus.write(0x4003, 0x02); // timer high => period 0x200
|
||||
bus.write(0x4001, 0x82); // enable, period=1, negate=0, shift=2
|
||||
|
||||
for _ in 0..7_457u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.read(0x4002), 0x80);
|
||||
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_negative_pulse1_uses_ones_complement() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4002, 0x00); // period 0x200
|
||||
bus.write(0x4003, 0x02);
|
||||
bus.write(0x4001, 0x8A); // enable, period=1, negate=1, shift=2
|
||||
|
||||
for _ in 0..7_457u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
assert_eq!(bus.apu.read(0x4002), 0x7F);
|
||||
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmc_dma_fetches_sample_bytes_and_steals_cpu_cycles() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4010, 0x0F); // no IRQ, no loop, fastest period
|
||||
bus.write(0x4012, 0x00); // sample start $C000
|
||||
bus.write(0x4013, 0x00); // sample length = 1 byte
|
||||
bus.write(0x4015, 0x10); // enable DMC (issues initial DMA request)
|
||||
|
||||
assert_eq!(bus.ppu_dot, 0);
|
||||
bus.clock_cpu(1);
|
||||
|
||||
// 1 CPU cycle + 4-cycle DMA steal = 5 total CPU cycles => 15 PPU dots.
|
||||
assert_eq!(bus.ppu_dot, 15);
|
||||
assert_eq!(bus.apu.dmc_bytes_remaining, 0);
|
||||
assert_eq!(bus.apu.dmc_current_addr, 0xC001);
|
||||
assert!(bus.apu.dmc_sample_buffer_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dmc_playback_updates_output_level_from_sample_bits() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x4011, 0x20); // initial DMC DAC level
|
||||
bus.write(0x4010, 0x0F); // fastest DMC rate
|
||||
bus.write(0x4012, 0x00); // sample start $C000
|
||||
bus.write(0x4013, 0x00); // 1-byte sample
|
||||
bus.write(0x4015, 0x10); // enable DMC
|
||||
|
||||
// Service initial DMA request.
|
||||
bus.clock_cpu(1);
|
||||
let initial = bus.apu.dmc_output_level;
|
||||
|
||||
// Stub mapper returns 0x00 sample byte, so each played bit drives output down by 2.
|
||||
for _ in 0..600u32 {
|
||||
bus.clock_cpu(1);
|
||||
}
|
||||
|
||||
assert!(bus.apu.dmc_output_level < initial);
|
||||
}
|
||||
Reference in New Issue
Block a user