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:
101
src/native_core/bus/cpu_bus_impl.rs
Normal file
101
src/native_core/bus/cpu_bus_impl.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use super::*;
|
||||
impl CpuBus for NativeBus {
|
||||
fn read(&mut self, addr: u16) -> u8 {
|
||||
let value = match addr {
|
||||
0x0000..=0x1FFF => self.cpu_ram[(addr as usize) & 0x07FF],
|
||||
0x2000..=0x3FFF => {
|
||||
let reg = (addr as u8) & 7;
|
||||
let scanline = self.ppu_dot / PPU_DOTS_PER_SCANLINE;
|
||||
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||
let value = if reg == 4 {
|
||||
let rendering_read_phase = self.ppu.rendering_enabled()
|
||||
&& (scanline < 240 || scanline == PPU_PRERENDER_SCANLINE)
|
||||
&& ((1..=256).contains(&dot) || (321..=340).contains(&dot));
|
||||
self.ppu.cpu_read_oamdata(rendering_read_phase)
|
||||
} else {
|
||||
let mapper: &(dyn Mapper + Send) = &*self.mapper;
|
||||
self.ppu.cpu_read(reg, mapper)
|
||||
};
|
||||
if reg == 2 {
|
||||
if scanline == PPU_VBLANK_START_SCANLINE && dot == 1 {
|
||||
self.suppress_vblank_this_frame = true;
|
||||
}
|
||||
// Reading PPUSTATUS clears VBlank; do not keep a stale NMI latched.
|
||||
self.nmi_pending = false;
|
||||
}
|
||||
value
|
||||
}
|
||||
0x4000..=0x4013 => self.cpu_open_bus,
|
||||
0x4015 => self.apu.read(addr),
|
||||
0x4016 => self.joypad_read(),
|
||||
0x4017 => self.joypad2_read(),
|
||||
0x6000..=0x7FFF => self.mapper.cpu_read_low(addr).unwrap_or(self.cpu_open_bus),
|
||||
0x8000..=0xFFFF => self.mapper.cpu_read(addr),
|
||||
_ => self.cpu_open_bus,
|
||||
};
|
||||
self.cpu_open_bus = value;
|
||||
value
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u16, value: u8) {
|
||||
self.cpu_open_bus = value;
|
||||
match addr {
|
||||
0x0000..=0x1FFF => self.cpu_ram[(addr as usize) & 0x07FF] = value,
|
||||
0x2000..=0x3FFF => {
|
||||
let reg = (addr as u8) & 7;
|
||||
let nmi_was_enabled = self.ppu.nmi_enabled();
|
||||
{
|
||||
let (ppu, mapper) = (&mut self.ppu, &mut self.mapper);
|
||||
ppu.cpu_write(reg, value, &mut **mapper);
|
||||
}
|
||||
if reg == 0
|
||||
&& !nmi_was_enabled
|
||||
&& self.ppu.nmi_enabled()
|
||||
&& self.ppu.vblank_flag_set()
|
||||
{
|
||||
self.nmi_pending = true;
|
||||
}
|
||||
if reg == 0 || reg == 5 {
|
||||
self.note_scroll_write_now();
|
||||
}
|
||||
}
|
||||
0x4000..=0x4013 => self.apu.write(addr, value),
|
||||
0x4015 => self.apu.write(addr, value),
|
||||
0x4017 => self.apu.write(addr, value),
|
||||
0x4014 => {
|
||||
self.apu.write(0x4014, value);
|
||||
let base = (value as u16) << 8;
|
||||
let mut dma = [0u8; 256];
|
||||
for i in 0..=u8::MAX {
|
||||
dma[i as usize] = self.dma_read(base.wrapping_add(i as u16));
|
||||
}
|
||||
for byte in dma {
|
||||
self.ppu.dma_write_oam(byte);
|
||||
}
|
||||
// OAM DMA stalls CPU for 513/514 cycles while PPU and mapper continue.
|
||||
let cpu_phase = (self.ppu_dot / 3) & 1;
|
||||
self.clock_cpu_cycles(513 + cpu_phase);
|
||||
}
|
||||
0x4016 => self.joypad_write(value),
|
||||
0x6000..=0x7FFF => {
|
||||
self.mapper.cpu_write_low(addr, value);
|
||||
}
|
||||
0x8000..=0xFFFF => self.mapper.cpu_write(addr, value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_nmi(&mut self) -> bool {
|
||||
let out = self.nmi_pending;
|
||||
self.nmi_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
if self.apu.poll_irq() {
|
||||
true
|
||||
} else {
|
||||
self.mapper.poll_irq()
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/native_core/bus/joypad.rs
Normal file
74
src/native_core/bus/joypad.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use super::NativeBus;
|
||||
|
||||
impl NativeBus {
|
||||
pub fn set_joypad_buttons(&mut self, buttons: [bool; 8]) {
|
||||
let mut state = 0u8;
|
||||
if buttons[4] {
|
||||
state |= 1 << 0; // A
|
||||
}
|
||||
if buttons[5] {
|
||||
state |= 1 << 1; // B
|
||||
}
|
||||
if buttons[7] {
|
||||
state |= 1 << 2; // Select
|
||||
}
|
||||
if buttons[6] {
|
||||
state |= 1 << 3; // Start
|
||||
}
|
||||
if buttons[0] {
|
||||
state |= 1 << 4; // Up
|
||||
}
|
||||
if buttons[1] {
|
||||
state |= 1 << 5; // Down
|
||||
}
|
||||
if buttons[2] {
|
||||
state |= 1 << 6; // Left
|
||||
}
|
||||
if buttons[3] {
|
||||
state |= 1 << 7; // Right
|
||||
}
|
||||
self.joypad_state = state;
|
||||
self.joypad2_state = 0;
|
||||
if self.joypad_strobe {
|
||||
self.joypad_shift = self.joypad_state;
|
||||
self.joypad2_shift = self.joypad2_state;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn joypad_read(&mut self) -> u8 {
|
||||
let bit = if self.joypad_strobe {
|
||||
self.joypad_state & 1
|
||||
} else {
|
||||
let bit = self.joypad_shift & 1;
|
||||
self.joypad_shift = (self.joypad_shift >> 1) | 0x80;
|
||||
bit
|
||||
};
|
||||
self.format_controller_read(bit)
|
||||
}
|
||||
|
||||
pub(super) fn joypad2_read(&mut self) -> u8 {
|
||||
let bit = if self.joypad_strobe {
|
||||
self.joypad2_state & 1
|
||||
} else {
|
||||
let bit = self.joypad2_shift & 1;
|
||||
self.joypad2_shift = (self.joypad2_shift >> 1) | 0x80;
|
||||
bit
|
||||
};
|
||||
self.format_controller_read(bit)
|
||||
}
|
||||
|
||||
pub(super) fn joypad_write(&mut self, value: u8) {
|
||||
let strobe = (value & 1) != 0;
|
||||
self.joypad_strobe = strobe;
|
||||
if strobe {
|
||||
self.joypad_shift = self.joypad_state;
|
||||
self.joypad2_shift = self.joypad2_state;
|
||||
}
|
||||
}
|
||||
|
||||
fn format_controller_read(&self, bit: u8) -> u8 {
|
||||
// Controller reads expose serial data in bit0, keep bit6 high, and
|
||||
// preserve open-bus upper bits.
|
||||
(self.cpu_open_bus & 0xE0) | 0x40 | (bit & 1)
|
||||
}
|
||||
}
|
||||
158
src/native_core/bus/state.rs
Normal file
158
src/native_core/bus/state.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use super::*;
|
||||
use crate::native_core::apu::ApuStateTail;
|
||||
use crate::native_core::state_io as sio;
|
||||
|
||||
const BUS_STATE_CTX: &str = "bus state";
|
||||
|
||||
impl NativeBus {
|
||||
pub fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.cpu_ram);
|
||||
self.ppu.save_state(out);
|
||||
out.extend_from_slice(self.apu.registers());
|
||||
out.push(self.cpu_open_bus);
|
||||
out.push(self.joypad_state);
|
||||
out.push(self.joypad_shift);
|
||||
out.push(self.joypad2_state);
|
||||
out.push(self.joypad2_shift);
|
||||
out.push(u8::from(self.joypad_strobe));
|
||||
out.push(u8::from(self.nmi_pending));
|
||||
out.extend_from_slice(&self.ppu_dot.to_le_bytes());
|
||||
out.push(u8::from(self.odd_frame));
|
||||
out.push(u8::from(self.in_vblank));
|
||||
out.push(u8::from(self.mmc3_a12_prev_high));
|
||||
out.extend_from_slice(&self.mmc3_a12_low_dots.to_le_bytes());
|
||||
out.extend_from_slice(&self.mmc3_last_irq_scanline.to_le_bytes());
|
||||
self.apu.save_state_tail(out);
|
||||
|
||||
let mut mapper_state = Vec::new();
|
||||
self.mapper.save_state(&mut mapper_state);
|
||||
out.extend_from_slice(&(mapper_state.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(&mapper_state);
|
||||
}
|
||||
|
||||
pub fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
let mut cursor = 0usize;
|
||||
|
||||
self.cpu_ram.copy_from_slice(sio::take_exact(
|
||||
data,
|
||||
&mut cursor,
|
||||
CPU_RAM_SIZE,
|
||||
BUS_STATE_CTX,
|
||||
)?);
|
||||
let ppu_consumed = self.ppu.load_state(&data[cursor..])?;
|
||||
cursor = cursor.saturating_add(ppu_consumed);
|
||||
let mut apu_regs = [0u8; 0x20];
|
||||
apu_regs.copy_from_slice(sio::take_exact(data, &mut cursor, 0x20, BUS_STATE_CTX)?);
|
||||
self.apu.set_registers(apu_regs);
|
||||
self.frame_complete = false;
|
||||
|
||||
self.cpu_open_bus = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad2_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad2_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.joypad_strobe = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.nmi_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.suppress_vblank_this_frame = false;
|
||||
self.ppu_dot = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
self.ppu_dot %= PPU_DOTS_PER_FRAME;
|
||||
self.odd_frame = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.in_vblank = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.mmc3_a12_prev_high = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.mmc3_a12_low_dots = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
self.mmc3_last_irq_scanline = u32::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let frame_cycle = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let channel_enable_mask = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let mut length_counters = [0u8; 4];
|
||||
length_counters.copy_from_slice(sio::take_exact(data, &mut cursor, 4, BUS_STATE_CTX)?);
|
||||
let dmc_bytes_remaining = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_irq_enabled = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_cycle_counter = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_current_addr = u16::from_le_bytes([
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||
]);
|
||||
let dmc_sample_buffer = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_sample_buffer_valid = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_shift_reg = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_bits_remaining = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_silence = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let dmc_output_level = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let dmc_dma_request = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let mut envelope_divider = [0u8; 3];
|
||||
envelope_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||
let mut envelope_decay = [0u8; 3];
|
||||
envelope_decay.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||
let envelope_start_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let triangle_linear_counter = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let triangle_linear_reload_flag = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let mut sweep_divider = [0u8; 2];
|
||||
sweep_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 2, BUS_STATE_CTX)?);
|
||||
let sweep_reload_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let cpu_cycle_parity = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_reset_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let frame_reset_delay = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||
let pending_frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
let pending_frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||
self.apu.load_state_tail(ApuStateTail {
|
||||
frame_cycle,
|
||||
frame_mode_5step,
|
||||
frame_irq_inhibit,
|
||||
frame_irq_pending,
|
||||
channel_enable_mask,
|
||||
length_counters,
|
||||
dmc_bytes_remaining,
|
||||
dmc_irq_enabled,
|
||||
dmc_irq_pending,
|
||||
dmc_cycle_counter,
|
||||
dmc_current_addr,
|
||||
dmc_sample_buffer,
|
||||
dmc_sample_buffer_valid,
|
||||
dmc_shift_reg,
|
||||
dmc_bits_remaining,
|
||||
dmc_silence,
|
||||
dmc_output_level,
|
||||
dmc_dma_request,
|
||||
envelope_divider,
|
||||
envelope_decay,
|
||||
envelope_start_flags,
|
||||
triangle_linear_counter,
|
||||
triangle_linear_reload_flag,
|
||||
sweep_divider,
|
||||
sweep_reload_flags,
|
||||
cpu_cycle_parity,
|
||||
frame_reset_pending,
|
||||
frame_reset_delay,
|
||||
pending_frame_mode_5step,
|
||||
pending_frame_irq_inhibit,
|
||||
});
|
||||
let mapper_len = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)? as usize;
|
||||
let mapper_state = sio::take_exact(data, &mut cursor, mapper_len, BUS_STATE_CTX)?;
|
||||
|
||||
self.ppu.set_vblank(self.in_vblank);
|
||||
self.mapper.load_state(mapper_state)?;
|
||||
|
||||
if cursor != data.len() {
|
||||
return Err("bus state: trailing bytes in payload".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
159
src/native_core/bus/tests.rs
Normal file
159
src/native_core/bus/tests.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use super::{
|
||||
CpuBus, NativeBus, PPU_DOTS_PER_SCANLINE, PPU_PRERENDER_SCANLINE, PPU_VBLANK_START_SCANLINE,
|
||||
};
|
||||
use crate::native_core::{ines::Mirroring, mapper::Mapper};
|
||||
|
||||
struct StubMapper;
|
||||
|
||||
impl Mapper for StubMapper {
|
||||
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||
|
||||
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ScanlineIrqMapper {
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl Mapper for ScanlineIrqMapper {
|
||||
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||
|
||||
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct A12GatedMapper {
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl Mapper for A12GatedMapper {
|
||||
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||
|
||||
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct A12CountMapper {
|
||||
clocks: u8,
|
||||
}
|
||||
|
||||
impl Mapper for A12CountMapper {
|
||||
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||
0
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.clocks = self.clocks.saturating_add(1);
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
if self.clocks == 0 {
|
||||
false
|
||||
} else {
|
||||
self.clocks -= 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||
|
||||
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod apu;
|
||||
mod mapper_timing;
|
||||
mod ppu_open_bus;
|
||||
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);
|
||||
}
|
||||
72
src/native_core/bus/tests/mapper_timing.rs
Normal file
72
src/native_core/bus/tests/mapper_timing.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prerender_scanline_still_clocks_mapper_scanline_irq() {
|
||||
let mut bus = NativeBus::new(Box::new(ScanlineIrqMapper { irq_pending: false }));
|
||||
bus.write(0x2001, 0x18); // enable rendering
|
||||
|
||||
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE + 259;
|
||||
bus.clock_ppu_dot(); // now at dot 260
|
||||
assert!(bus.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mmc3_class_scanline_clock_is_suppressed_when_a12_has_no_activity() {
|
||||
let mut bus = NativeBus::new(Box::new(A12GatedMapper { irq_pending: false }));
|
||||
bus.write(0x2001, 0x18); // BG+sprites on
|
||||
bus.write(0x2000, 0x00); // BG table $0000, sprite table $0000, 8x8 sprites
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE;
|
||||
for _ in 0..120 {
|
||||
bus.clock_ppu_dot();
|
||||
}
|
||||
assert!(!bus.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mmc3_class_scanline_clock_runs_when_pattern_table_uses_a12() {
|
||||
let mut bus = NativeBus::new(Box::new(A12GatedMapper { irq_pending: false }));
|
||||
bus.write(0x2001, 0x18); // BG+sprites on
|
||||
bus.write(0x2000, 0x10); // BG pattern table $1000
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE;
|
||||
for _ in 0..120 {
|
||||
bus.clock_ppu_dot();
|
||||
}
|
||||
assert!(bus.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mmc3_class_a12_filter_clocks_once_per_scanline() {
|
||||
let mut bus = NativeBus::new(Box::new(A12CountMapper { clocks: 0 }));
|
||||
bus.write(0x2001, 0x08); // BG on
|
||||
bus.write(0x2000, 0x10); // BG pattern table $1000
|
||||
|
||||
bus.ppu_dot = 30 * PPU_DOTS_PER_SCANLINE;
|
||||
for _ in 0..PPU_DOTS_PER_SCANLINE {
|
||||
bus.clock_ppu_dot();
|
||||
}
|
||||
|
||||
let mut count = 0u8;
|
||||
while bus.poll_irq() {
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_roundtrip_preserves_mmc3_a12_timing_fields() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.mmc3_a12_prev_high = true;
|
||||
bus.mmc3_a12_low_dots = 3;
|
||||
bus.mmc3_last_irq_scanline = 123;
|
||||
|
||||
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!(restored.mmc3_a12_prev_high);
|
||||
assert_eq!(restored.mmc3_a12_low_dots, 3);
|
||||
assert_eq!(restored.mmc3_last_irq_scanline, 123);
|
||||
}
|
||||
191
src/native_core/bus/tests/ppu_open_bus.rs
Normal file
191
src/native_core/bus/tests/ppu_open_bus.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reading_ppustatus_clears_latched_nmi_request() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2000, 0x80); // enable NMI on VBlank
|
||||
|
||||
for _ in 0..100_000usize {
|
||||
bus.clock_cpu(1);
|
||||
if bus.nmi_pending {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(bus.nmi_pending, "vblank NMI should have latched");
|
||||
|
||||
let _ = bus.read(0x2002);
|
||||
assert!(!bus.nmi_pending, "status read should clear pending NMI");
|
||||
assert!(
|
||||
!bus.poll_nmi(),
|
||||
"CPU should not observe stale NMI after status read"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_overflow_flag_is_set_during_rendering_and_cleared_prerender() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2001, 0x18); // enable BG + sprites rendering
|
||||
|
||||
// Initialize OAM offscreen, then place 9 sprites on scanline 20.
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..256usize {
|
||||
bus.write(0x2004, 0xFF);
|
||||
}
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..9usize {
|
||||
bus.write(0x2004, 19); // Y (sprite appears at Y+1 = 20)
|
||||
bus.write(0x2004, 0); // tile
|
||||
bus.write(0x2004, 0); // attr
|
||||
bus.write(0x2004, 0); // X
|
||||
}
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||
bus.clock_ppu_dot(); // dot 257 -> overflow evaluation
|
||||
assert!(bus.ppu.sprite_overflow_set());
|
||||
|
||||
bus.in_vblank = true;
|
||||
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE;
|
||||
bus.clock_ppu_dot(); // prerender dot 1 -> clear status flags
|
||||
assert!(!bus.ppu.sprite_overflow_set());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_overflow_latches_until_prerender_clear() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2001, 0x18); // rendering enabled
|
||||
|
||||
// Populate first 9 sprites on scanline 20, others offscreen.
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..256usize {
|
||||
bus.write(0x2004, 0xFF);
|
||||
}
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..9usize {
|
||||
bus.write(0x2004, 19); // Y => visible on scanline 20
|
||||
bus.write(0x2004, 0);
|
||||
bus.write(0x2004, 0);
|
||||
bus.write(0x2004, 0);
|
||||
}
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||
bus.clock_ppu_dot(); // scanline 20 dot 257
|
||||
assert!(bus.ppu.sprite_overflow_set());
|
||||
|
||||
// Move to a scanline with no overflow and ensure bit stays latched.
|
||||
bus.ppu_dot = 100 * PPU_DOTS_PER_SCANLINE + 256;
|
||||
bus.clock_ppu_dot();
|
||||
assert!(bus.ppu.sprite_overflow_set());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_overflow_not_evaluated_when_sprites_disabled() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2001, 0x08); // BG enabled, sprites disabled
|
||||
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..256usize {
|
||||
bus.write(0x2004, 0xFF);
|
||||
}
|
||||
bus.write(0x2003, 0x00);
|
||||
for _ in 0..9usize {
|
||||
bus.write(0x2004, 19);
|
||||
bus.write(0x2004, 0);
|
||||
bus.write(0x2004, 0);
|
||||
bus.write(0x2004, 0);
|
||||
}
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||
bus.clock_ppu_dot();
|
||||
assert!(!bus.ppu.sprite_overflow_set());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_frame_skips_one_ppu_dot_when_rendering_enabled() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2001, 0x18); // rendering enabled
|
||||
|
||||
let mut dots = 0u32;
|
||||
while !bus.frame_complete {
|
||||
bus.clock_ppu_dot();
|
||||
dots += 1;
|
||||
}
|
||||
assert_eq!(dots, 341 * 262); // even frame
|
||||
bus.frame_complete = false;
|
||||
|
||||
let mut odd_frame_dots = 0u32;
|
||||
while !bus.frame_complete {
|
||||
bus.clock_ppu_dot();
|
||||
odd_frame_dots += 1;
|
||||
}
|
||||
assert_eq!(odd_frame_dots, 341 * 262 - 1); // odd frame skip
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmapped_cpu_reads_return_open_bus_value() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
|
||||
bus.write(0x0000, 0xAB); // write puts value on CPU bus
|
||||
assert_eq!(bus.read(0x4018), 0xAB); // unmapped APU/test range
|
||||
|
||||
bus.write(0x0001, 0xCD);
|
||||
assert_eq!(bus.read(0x4FFF), 0xCD); // unmapped expansion range
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joypad_read_preserves_open_bus_upper_bits_and_sets_bit6() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.set_joypad_buttons([false, false, false, false, true, false, false, false]); // A pressed
|
||||
bus.write(0x4016, 0xA1); // strobe on + seed open bus high bits
|
||||
let v = bus.read(0x4016);
|
||||
assert_eq!(v & 0x01, 1);
|
||||
assert_eq!(v & 0x40, 0x40);
|
||||
assert_eq!(v & 0xE0, 0xE0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ppustatus_read_at_vblank_edge_suppresses_vblank_for_that_frame() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2000, 0x80); // enable NMI
|
||||
|
||||
// Position exactly at vblank set point (scanline 241 dot 1).
|
||||
bus.ppu_dot = PPU_VBLANK_START_SCANLINE * PPU_DOTS_PER_SCANLINE + 1;
|
||||
let _ = bus.read(0x2002); // status read at dot 1 suppresses vblank for this frame
|
||||
bus.clock_ppu_dot(); // dot 2 on scanline 241
|
||||
|
||||
assert!(!bus.in_vblank);
|
||||
assert!(!bus.nmi_pending);
|
||||
let status = bus.read(0x2002);
|
||||
assert_eq!(status & 0x80, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prerender_clears_status_flags_even_if_not_in_vblank() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.in_vblank = false;
|
||||
bus.ppu.set_vblank(true);
|
||||
bus.ppu.set_sprite0_hit(true);
|
||||
bus.ppu.set_sprite_overflow(true);
|
||||
|
||||
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE;
|
||||
bus.clock_ppu_dot(); // prerender dot 1
|
||||
|
||||
let status = bus.read(0x2002);
|
||||
assert_eq!(status & 0xE0, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oamdata_read_returns_ff_during_active_rendering() {
|
||||
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||
bus.write(0x2001, 0x18); // rendering enabled
|
||||
bus.write(0x2003, 0x10);
|
||||
bus.write(0x2004, 0x22);
|
||||
bus.write(0x2003, 0x10);
|
||||
|
||||
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 100;
|
||||
let v = bus.read(0x2004);
|
||||
assert_eq!(v, 0xFF);
|
||||
|
||||
bus.ppu_dot = PPU_VBLANK_START_SCANLINE * PPU_DOTS_PER_SCANLINE + 10;
|
||||
let v2 = bus.read(0x2004);
|
||||
assert_eq!(v2, 0x22);
|
||||
}
|
||||
116
src/native_core/bus/timing.rs
Normal file
116
src/native_core/bus/timing.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use super::{
|
||||
NativeBus, PPU_DOTS_PER_FRAME, PPU_DOTS_PER_SCANLINE, PPU_PRERENDER_SCANLINE,
|
||||
PPU_VBLANK_START_SCANLINE,
|
||||
};
|
||||
use crate::native_core::cpu::CpuBus;
|
||||
use crate::native_core::mapper::Mapper;
|
||||
|
||||
impl NativeBus {
|
||||
fn clock_one_cpu_cycle(&mut self) {
|
||||
for _ in 0..3 {
|
||||
self.clock_ppu_dot();
|
||||
}
|
||||
self.mapper.clock_cpu(1);
|
||||
self.apu.clock_cpu_cycle();
|
||||
}
|
||||
|
||||
fn service_dmc_dma(&mut self, addr: u16) {
|
||||
let byte = self.dma_read(addr);
|
||||
self.apu.provide_dmc_dma_byte(byte);
|
||||
// DMC DMA steals CPU bus cycles while APU/PPU/mapper keep ticking.
|
||||
for _ in 0..4 {
|
||||
self.clock_one_cpu_cycle();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dma_read(&mut self, addr: u16) -> u8 {
|
||||
<Self as CpuBus>::read(self, addr)
|
||||
}
|
||||
|
||||
pub(super) fn clock_ppu_dot(&mut self) {
|
||||
self.ppu_dot += 1;
|
||||
if self.ppu_dot >= PPU_DOTS_PER_FRAME {
|
||||
self.ppu_dot = 0;
|
||||
self.frame_complete = true;
|
||||
self.odd_frame = !self.odd_frame;
|
||||
}
|
||||
|
||||
let scanline = self.ppu_dot / PPU_DOTS_PER_SCANLINE;
|
||||
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||
let rendering_enabled = self.ppu.rendering_enabled();
|
||||
|
||||
{
|
||||
let mapper: &(dyn Mapper + Send) = &*self.mapper;
|
||||
self.ppu.render_dot(mapper, scanline, dot);
|
||||
}
|
||||
|
||||
if rendering_enabled && (scanline < 240 || scanline == PPU_PRERENDER_SCANLINE) {
|
||||
if self.mapper.needs_ppu_a12_clock() {
|
||||
let a12_high = self.ppu.mmc3_a12_high_at(scanline, dot);
|
||||
if a12_high {
|
||||
if !self.mmc3_a12_prev_high
|
||||
&& self.mmc3_a12_low_dots >= 8
|
||||
&& self.mmc3_last_irq_scanline != scanline
|
||||
{
|
||||
self.mapper.clock_scanline();
|
||||
self.mmc3_last_irq_scanline = scanline;
|
||||
}
|
||||
self.mmc3_a12_prev_high = true;
|
||||
self.mmc3_a12_low_dots = 0;
|
||||
} else {
|
||||
self.mmc3_a12_prev_high = false;
|
||||
self.mmc3_a12_low_dots = self.mmc3_a12_low_dots.saturating_add(1);
|
||||
}
|
||||
} else if dot == 260 {
|
||||
self.mapper.clock_scanline();
|
||||
}
|
||||
} else {
|
||||
self.mmc3_a12_prev_high = false;
|
||||
self.mmc3_a12_low_dots = self.mmc3_a12_low_dots.saturating_add(1);
|
||||
}
|
||||
if rendering_enabled && scanline == PPU_PRERENDER_SCANLINE && dot == 339 && self.odd_frame {
|
||||
// NTSC odd frame timing: skip pre-render dot 340 when rendering is enabled.
|
||||
self.ppu_dot = self.ppu_dot.saturating_add(1);
|
||||
}
|
||||
|
||||
if !self.in_vblank && scanline == PPU_VBLANK_START_SCANLINE && dot == 1 {
|
||||
if self.suppress_vblank_this_frame {
|
||||
self.suppress_vblank_this_frame = false;
|
||||
self.in_vblank = false;
|
||||
self.ppu.set_vblank(false);
|
||||
} else {
|
||||
self.in_vblank = true;
|
||||
self.ppu.set_vblank(true);
|
||||
if self.ppu.nmi_enabled() {
|
||||
self.nmi_pending = true;
|
||||
}
|
||||
}
|
||||
} else if scanline == PPU_PRERENDER_SCANLINE && dot == 1 {
|
||||
self.in_vblank = false;
|
||||
self.ppu.set_vblank(false);
|
||||
self.ppu.set_sprite0_hit(false);
|
||||
self.ppu.set_sprite_overflow(false);
|
||||
self.suppress_vblank_this_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clock_cpu_cycles(&mut self, cycles: u32) {
|
||||
if cycles == 0 {
|
||||
return;
|
||||
}
|
||||
let mut remaining = cycles;
|
||||
while remaining != 0 {
|
||||
self.clock_one_cpu_cycle();
|
||||
while let Some(addr) = self.apu.take_dmc_dma_request() {
|
||||
self.service_dmc_dma(addr);
|
||||
}
|
||||
remaining -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn note_scroll_write_now(&mut self) {
|
||||
let scanline = (self.ppu_dot / PPU_DOTS_PER_SCANLINE) as usize;
|
||||
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||
self.ppu.note_scroll_register_write(scanline, dot);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user