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..29_832u32 { 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..29_832u32 { 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..29_832u32 { 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..29_832u32 { 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..29_832u32 { 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..14_913u32 { bus.clock_cpu(1); } assert_eq!(bus.apu.length_counters[0], 1); for _ in 0..14_916u32 { 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..7_457u32 { bus.clock_cpu(1); } assert_eq!(bus.apu.triangle_linear_counter, 5); assert!(!bus.apu.triangle_linear_reload_flag); for _ in 0..7_456u32 { 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..7_457u32 { 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..14_913u32 { 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..14_913u32 { 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); } #[test] fn pulse_channel_outputs_become_audible_after_setup() { let mut bus = NativeBus::new(Box::new(StubMapper)); bus.write(0x4015, 0x01); // enable pulse1 bus.write(0x4000, 0b0101_1111); // 25% duty, constant volume=15 bus.write(0x4002, 0x08); // low timer period, not sweep-muted bus.write(0x4003, 0x00); // reload length + reset duty sequencer let mut saw_non_zero = false; for _ in 0..64u32 { bus.clock_cpu(1); if bus.apu_channel_outputs().pulse1 > 0 { saw_non_zero = true; break; } } assert!(saw_non_zero, "pulse1 never produced audible output"); }