diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..0215e9a --- /dev/null +++ b/src/io.rs @@ -0,0 +1,383 @@ +//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and polled input. +//! Timer phase resets on enable or clock-source changes. Hardware startup delays, +//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled. + +use crate::{ + display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, + dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END}, + input::Buttons, +}; + +pub const DISPCNT: u32 = 0x0400_0000; +pub const GREENSWAP: u32 = 0x0400_0002; +pub const DISPSTAT: u32 = 0x0400_0004; +pub const VCOUNT: u32 = 0x0400_0006; +pub const BG0CNT: u32 = 0x0400_0008; +pub const BG1CNT: u32 = 0x0400_000a; +pub const BG2CNT: u32 = 0x0400_000c; +pub const BG3CNT: u32 = 0x0400_000e; +pub const BG0HOFS: u32 = 0x0400_0010; +pub const BG0VOFS: u32 = 0x0400_0012; +pub const BG1HOFS: u32 = 0x0400_0014; +pub const BG1VOFS: u32 = 0x0400_0016; +pub const BG2HOFS: u32 = 0x0400_0018; +pub const BG2VOFS: u32 = 0x0400_001a; +pub const BG3HOFS: u32 = 0x0400_001c; +pub const BG3VOFS: u32 = 0x0400_001e; +pub const BG2PA: u32 = 0x0400_0020; +pub const BG2PB: u32 = 0x0400_0022; +pub const BG2PC: u32 = 0x0400_0024; +pub const BG2PD: u32 = 0x0400_0026; +pub const BG2X: u32 = 0x0400_0028; +pub const BG2Y: u32 = 0x0400_002c; +pub const BG3PA: u32 = 0x0400_0030; +pub const BG3PB: u32 = 0x0400_0032; +pub const BG3PC: u32 = 0x0400_0034; +pub const BG3PD: u32 = 0x0400_0036; +pub const BG3X: u32 = 0x0400_0038; +pub const BG3Y: u32 = 0x0400_003c; +pub const WIN0H: u32 = 0x0400_0040; +pub const WIN1H: u32 = 0x0400_0042; +pub const WIN0V: u32 = 0x0400_0044; +pub const WIN1V: u32 = 0x0400_0046; +pub const WININ: u32 = 0x0400_0048; +pub const WINOUT: u32 = 0x0400_004a; +pub const MOSAIC: u32 = 0x0400_004c; +pub const BLDCNT: u32 = 0x0400_0050; +pub const BLDALPHA: u32 = 0x0400_0052; +pub const BLDY: u32 = 0x0400_0054; +pub const KEYINPUT: u32 = 0x0400_0130; +pub const TIMER_BASE: u32 = 0x0400_0100; +pub const IE: u32 = 0x0400_0200; +pub const IF: u32 = 0x0400_0202; +pub const WAITCNT: u32 = 0x0400_0204; +pub const IME: u32 = 0x0400_0208; +pub const POSTFLG: u32 = 0x0400_0300; +pub const HALTCNT: u32 = 0x0400_0301; + +const IRQ_MASK: u16 = 0x3fff; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +struct Timer { + reload: u16, + counter: u16, + control: u8, + phase: u32, +} + +impl Timer { + fn enabled(&self) -> bool { + self.control & 0x80 != 0 + } + + fn count_up(&self) -> bool { + self.control & 4 != 0 + } + + fn set_control(&mut self, value: u8, index: usize) { + let value = value & if index == 0 { 0xc3 } else { 0xc7 }; + let start = !self.enabled() && value & 0x80 != 0; + if start { + self.counter = self.reload; + } + if start || (self.control ^ value) & 7 != 0 { + self.phase = 0; + } + self.control = value; + } + + /// Next independently clocked overflow. Cascaded timers receive their + /// pulses at a predecessor's event and cannot overflow earlier. + fn next_overflow(&self) -> Option { + if !self.enabled() || self.count_up() { + return None; + } + let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)]; + Some((0x1_0000 - u32::from(self.counter)) * divisor - self.phase) + } + + /// Advance without looping once per cycle or overflow. Return the number of + /// overflows, not just a boolean, so cascaded timers receive every pulse. + fn advance(&mut self, cycles: u32, previous_overflows: u64) -> u64 { + if !self.enabled() { + return 0; + } + let ticks = if self.count_up() { + previous_overflows + } else { + let divisor = [1, 64, 256, 1024][usize::from(self.control & 3)]; + let elapsed = u64::from(self.phase) + u64::from(cycles); + self.phase = (elapsed % divisor) as u32; + elapsed / divisor + }; + let until_overflow = 0x1_0000 - u64::from(self.counter); + if ticks < until_overflow { + self.counter += ticks as u16; + return 0; + } + let remaining = ticks - until_overflow; + let period = 0x1_0000 - u64::from(self.reload); + self.counter = (u64::from(self.reload) + remaining % period) as u16; + 1 + remaining / period + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct Io { + timers: [Timer; 4], + enable: u16, + pending: u16, + master_enable: bool, + waitcnt: u16, + dispcnt: u16, + greenswap: bool, + mosaic: u16, + vertical_mosaic: crate::video::mosaic::VerticalMosaic, + backgrounds: [crate::video::Background; 4], + effects: crate::video::effects::Effects, + window_edges: crate::video::windows::WindowEdges, + horizontal_windows: crate::video::windows::horizontal::HorizontalWindows, + buttons: Buttons, + display: Display, + dma: Dma, + postflg: bool, + halted: bool, +} + +impl Io { + pub(crate) fn mapped(address: u32) -> bool { + matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0131 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303) + } + + pub(crate) fn read8(&self, address: u32) -> Option { + if (DMA_BASE..=DMA_END).contains(&address) { + return Some(self.dma.read8(address)); + } + let value = match address & !1 { + DISPCNT => self.dispcnt, + GREENSWAP => u16::from(self.greenswap), + DISPSTAT => self.display.status(), + VCOUNT => self.display.position().scanline, + BG0CNT..=BG3CNT => self.backgrounds[((address - BG0CNT) / 2) as usize].control, + BG0HOFS..=0x0400_003e => 0, // Scroll/affine registers are write-only; not open bus. + WIN0H..=WIN1V | MOSAIC | 0x0400_004e | BLDY | 0x0400_0056 => 0, // Write-only/unused; no open bus yet. + WININ => self.effects.inside, + WINOUT => self.effects.outside, + BLDCNT => self.effects.control, + BLDALPHA => self.effects.alpha, + KEYINPUT => self.buttons.keyinput(), + 0x0400_0100..=0x0400_010e => { + let timer = &self.timers[((address - TIMER_BASE) / 4) as usize]; + if address & 2 == 0 { + timer.counter + } else { + u16::from(timer.control) + } + } + IE => self.enable, + IF => self.pending, + WAITCNT => self.waitcnt, + 0x0400_0206 => 0, + IME => u16::from(self.master_enable), + 0x0400_020a => 0, + POSTFLG => u16::from(self.postflg), // HALTCNT reads zero, not an open-bus model. + 0x0400_0302 => 0, + _ => return None, + }; + Some(value.to_le_bytes()[(address & 1) as usize]) + } + + /// The memory bus validates every byte before calling this method. + pub(crate) fn write8(&mut self, address: u32, value: u8) { + debug_assert!(Self::mapped(address)); + if (DMA_BASE..=DMA_END).contains(&address) { + self.dma.write8(address, value); + return; + } + match address & !1 { + DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7, + GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0, + DISPSTAT => self.pending |= self.display.write_status(address & 1 != 0, value), + VCOUNT | KEYINPUT => {} // Read-only; writes are ignored. + BG0CNT..=BG3CNT => { + let index = ((address - BG0CNT) / 2) as usize; + let control = &mut self.backgrounds[index].control; + // Overflow applies to affine BG2/BG3 only; bits 4 and 5 are unused. + *control = replace_byte(*control, address, value) + & if index < 2 { 0xdfcf } else { 0xffcf }; + } + BG0HOFS..=BG3VOFS => { + let bg = &mut self.backgrounds[((address - BG0HOFS) / 4) as usize]; + let offset = if address & 2 == 0 { + &mut bg.x + } else { + &mut bg.y + }; + *offset = replace_byte(*offset, address, value) & 0x1ff; + } + BG2PA..=0x0400_003e => { + let index = 2 + ((address - BG2PA) / 16) as usize; + let position = self.display.position(); + let after_row = u32::from(position.line_cycle) >= HBLANK_START + || u32::from(position.scanline) >= VISIBLE_LINES; + self.backgrounds[index] + .affine + .write8((address & 15) as usize, value, after_row); + } + WIN0H..=WIN1V => { + let index = ((address - WIN0H) / 2) as usize; + let bounds = if index < 2 { + &mut self.effects.horizontal[index] + } else { + &mut self.effects.vertical[index - 2] + }; + *bounds = replace_byte(*bounds, address, value); + } + MOSAIC => self.mosaic = replace_byte(self.mosaic, address, value), + WININ | WINOUT | BLDCNT | BLDALPHA | BLDY => { + let (latch, mask) = match address & !1 { + WININ => (&mut self.effects.inside, 0x3f3f), + WINOUT => (&mut self.effects.outside, 0x3f3f), + BLDCNT => (&mut self.effects.control, 0x3fff), + BLDALPHA => (&mut self.effects.alpha, 0x1f1f), + _ => (&mut self.effects.brightness, 0x1f), + }; + *latch = replace_byte(*latch, address, value) & mask; + } + 0x0400_0100..=0x0400_010e => { + let index = ((address - TIMER_BASE) / 4) as usize; + let timer = &mut self.timers[index]; + if address & 2 == 0 { + // Merge into the reload latch, never the visible counter. + timer.reload = replace_byte(timer.reload, address, value); + } else if address & 1 == 0 { + timer.set_control(value, index); + } + } + IE => self.enable = replace_byte(self.enable, address, value) & IRQ_MASK, + WAITCNT => self.waitcnt = replace_byte(self.waitcnt, address, value) & 0x5fff, + IF => self.pending &= !(u16::from(value) << ((address & 1) * 8)), + IME if address & 1 == 0 => self.master_enable = value & 1 != 0, + POSTFLG if address == POSTFLG => self.postflg = value & 1 != 0, + POSTFLG => { + // The bus has already rejected STOP and enforced CPU write access. + debug_assert_eq!(value & 0x80, 0); + self.halted = true; + } + _ => {} // Unused I/O bytes ignore writes. + } + self.wake_if_requested(); + } + + pub(crate) fn halted(&self) -> bool { + self.halted + } + + fn wake_if_requested(&mut self) { + // Wake-up ignores IME and CPSR.I. It does not acknowledge IF. + if self.enable & self.pending != 0 { + self.halted = false; + } + } + + /// Bound an idle batch by the next display edge or independent timer overflow. + /// Display edges keep this finite even without an enabled wake source. + pub(crate) fn next_event_cycles(&self) -> u32 { + self.timers + .iter() + .filter_map(Timer::next_overflow) + .fold(self.display.next_event_cycles(), u32::min) + } + + pub(crate) fn display_position(&self) -> DisplayPosition { + self.display.position() + } + + pub(crate) fn set_buttons(&mut self, buttons: Buttons) { + self.buttons = buttons; + } + + pub(crate) fn display_control(&self) -> u16 { + self.dispcnt + } + + pub(crate) fn video_settings(&self, captured: bool) -> crate::video::RenderSettings<'_> { + crate::video::RenderSettings { + control: self.dispcnt, + prepared_objects: None, + internal_affine: captured, + vertical_windows: captured.then_some(self.window_edges.active), + horizontal_windows: captured.then_some(&self.horizontal_windows), + green_swap: self.greenswap, + mosaic: self.mosaic, + vertical_mosaic: captured.then_some(self.vertical_mosaic), + backgrounds: &self.backgrounds, + effects: &self.effects, + } + } + + pub(crate) fn waitcnt(&self) -> u16 { + self.waitcnt + } + + pub(crate) fn irq_pending(&self) -> bool { + self.master_enable && self.enable & self.pending != 0 + } + + pub(crate) fn next_dma(&self) -> Result, DmaError> { + self.dma.next() + } + + pub(crate) fn complete_dma_unit(&mut self, channel: usize) { + self.pending |= self.dma.complete_unit(channel); + self.wake_if_requested(); + } + + pub(crate) fn advance(&mut self, cycles: u32) { + // Hardware state tracks time even when host-side frame capture is disabled. + let position = self.display.position(); + let phase = u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle); + self.window_edges + .advance(phase, cycles, self.effects.vertical); + self.horizontal_windows + .advance(phase, cycles, self.effects.horizontal); + let mode = self.dispcnt & 7; + for index in 2..4 { + let active = self.dispcnt & (0x100 << index) != 0 + && if index == 2 { + (1..=5).contains(&mode) + } else { + mode == 2 + }; + let (height, counter) = if self.backgrounds[index].control & 0x40 != 0 { + ( + u32::from((self.mosaic >> 4) & 15) + 1, + self.vertical_mosaic.background, + ) + } else { + (1, 0) + }; + self.backgrounds[index] + .affine + .advance(phase, cycles, active, height, counter); + } + self.vertical_mosaic.advance(phase, cycles, self.mosaic); + let (vblank, hblank) = self.display.dma_events(cycles); + self.dma.trigger(vblank, hblank); + self.pending |= self.display.advance(cycles); + let mut overflows = 0; + for (index, timer) in self.timers.iter_mut().enumerate() { + overflows = timer.advance(cycles, overflows); + if overflows != 0 && timer.control & 0x40 != 0 { + // Pending flags latch even if IE, IME, or CPSR.I blocks delivery. + self.pending |= 1 << (3 + index); + } + } + self.wake_if_requested(); + } +} + +fn replace_byte(previous: u16, address: u32, value: u8) -> u16 { + let shift = (address & 1) * 8; + (previous & !(0xff << shift)) | (u16::from(value) << shift) +} diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..be71766 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,705 @@ +use std::{cell::Cell, error::Error, fmt}; + +use crate::{ + display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, + dma::{DmaError, DMA_BASE, DMA_END}, + input::Buttons, + io::{Io, HALTCNT, POSTFLG}, + timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming}, + video::{self, sprites::pipeline, Framebuffer, VideoError}, +}; + +pub const ROM_START: u32 = 0x0800_0000; +const ROM_CAPACITY: usize = 32 * 1024 * 1024; +pub const BIOS_SIZE: usize = 16 * 1024; +pub const PALETTE_START: u32 = 0x0500_0000; +pub const VRAM_START: u32 = 0x0600_0000; +pub const OAM_START: u32 = 0x0700_0000; +const VRAM_SIZE: usize = 96 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MemoryError { + Unmapped(u32), + ReadOnly(u32), + Unaligned(u32), + RomTooLarge(usize), + InvalidBiosSize(usize), + UnsupportedStop, +} + +impl fmt::Display for MemoryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedStop => write!(f, "HALTCNT STOP mode is not implemented"), + Self::Unmapped(address) => write!(f, "unmapped memory at {address:#010x}"), + Self::ReadOnly(address) => write!(f, "read-only memory at {address:#010x}"), + Self::Unaligned(address) => write!(f, "unaligned memory access at {address:#010x}"), + Self::RomTooLarge(size) => write!(f, "ROM size {size} exceeds 32 MiB"), + Self::InvalidBiosSize(size) => { + write!(f, "BIOS size {size} must be exactly {BIOS_SIZE} bytes") + } + } + } +} + +impl Error for MemoryError {} + +fn vram_index(address: u32) -> usize { + let offset = (address & 0x1ffff) as usize; + if offset >= 0x18000 { + offset - 0x8000 + } else { + offset + } +} + +/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O. +/// BIOS read protection and open-bus behavior are not modeled. +pub struct Memory { + external_ram: Vec, + internal_ram: Vec, + palette_ram: Vec, + video_ram: Vec, + oam: Vec, + rom: Vec, + bios: Option>, + io: Io, + cycles: u64, + data_timing: Cell>, + cpu_access: Option, + scanline_capture: Option, + sprite_pipeline: pipeline::SpritePipeline, +} + +impl Memory { + pub fn new(rom: Vec) -> Result { + if rom.len() > ROM_CAPACITY { + return Err(MemoryError::RomTooLarge(rom.len())); + } + Ok(Self { + external_ram: vec![0; 256 * 1024], + internal_ram: vec![0; 32 * 1024], + palette_ram: vec![0; 1024], + video_ram: vec![0; VRAM_SIZE], + oam: vec![0; 1024], + rom, + bios: None, + io: Io::default(), + cycles: 0, + data_timing: Cell::new(None), + cpu_access: None, + scanline_capture: None, + sprite_pipeline: pipeline::SpritePipeline::default(), + }) + } + + /// Map a caller-supplied 16 KiB image at the ARM exception vectors. + /// No Nintendo BIOS is included. The image is read-only and is not mirrored. + pub fn with_bios(rom: Vec, bios: Vec) -> Result { + if bios.len() != BIOS_SIZE { + return Err(MemoryError::InvalidBiosSize(bios.len())); + } + let mut memory = Self::new(rom)?; + memory.bios = Some(bios); + Ok(memory) + } + + /// Total supplied clock cycles, wrapping at u64::MAX. Reads/writes alone + /// consume no cycles. Machine stepping or the caller must advance time. + pub fn cycles(&self) -> u64 { + self.cycles + } + + /// Advance devices and optional row capture. Multiple unserviced DMA requests + /// coalesce. No CPU or DMA work is executed by this method. + pub fn advance_cycles(&mut self, cycles: u32) { + if cycles != 0 && !self.sprite_pipeline.started { + // Synthetic reset begins on row0, with no preceding row227 to prepare it. + self.prepare_sprite_row(0); + self.sprite_pipeline.started = true; + } + if self.scanline_capture.is_none() { + let position = self.io.display_position(); + let phase = + u32::from(position.scanline) * CYCLES_PER_LINE + u32::from(position.line_cycle); + let mut advanced = 0; + for (offset, row) in pipeline::latest_events(phase, cycles).into_iter().flatten() { + self.io.advance(offset - advanced); + self.prepare_sprite_row(row); + advanced = offset; + } + self.io.advance(cycles - advanced); + } else { + // Split at display and sprite-preparation edges. CPU/DMA writes have + // already committed before these nominal clocks; no per-access timing. + let mut remaining = cycles; + while remaining != 0 { + let position = self.io.display_position(); + let cycle = u32::from(position.line_cycle); + let display_edge = if cycle < HBLANK_START { + HBLANK_START - cycle + } else { + CYCLES_PER_LINE - cycle + }; + let until = + display_edge.min(pipeline::next_event(position.scanline, position.line_cycle)); + let step = remaining.min(until); + self.io.advance(step); + remaining -= step; + if step == until { + let position = self.io.display_position(); + if u32::from(position.line_cycle) == pipeline::PREPARE_CYCLE { + if let Some(row) = pipeline::target(position.scanline) { + self.prepare_sprite_row(row); + } + } else if u32::from(position.line_cycle) == HBLANK_START + && u32::from(position.scanline) < VISIBLE_LINES + { + let row = usize::from(position.scanline); + if self.scanline_capture.as_ref().unwrap().wants_row(row) { + let mut settings = self.io.video_settings(true); + settings.prepared_objects = Some(self.sprite_pipeline.row(row)); + let pixels = video::render_rows( + settings, + &self.video_ram, + &self.palette_ram, + &self.oam, + row..row + 1, + ); + self.scanline_capture.as_mut().unwrap().record(row, pixels); + } + } else if position.line_cycle == 0 + && u32::from(position.scanline) == VISIBLE_LINES + { + self.scanline_capture + .as_mut() + .unwrap() + .publish(position.vblanks); + } + } + } + } + self.cycles = self.cycles.wrapping_add(u64::from(cycles)); + } + + fn prepare_sprite_row(&mut self, row: usize) { + let settings = self.io.video_settings(true); + let prepared = video::sprites::prepare( + settings.control, + settings.mosaic, + settings.vertical_mosaic.map(|v| v.object), + &self.oam, + &self.video_ram, + row..row + 1, + ); + self.sprite_pipeline.store(row, prepared); + } + + /// Enable row capture without changing clocks or hardware registers. Disabled + /// by default to keep CPU-only tests and large clock batches inexpensive. + /// Repeating the current setting preserves captured data. Disabling drops it. + pub fn set_scanline_rendering(&mut self, enabled: bool) { + if enabled && self.scanline_capture.is_none() { + self.scanline_capture = Some(video::capture::Capture::default()); + } else if !enabled { + self.scanline_capture = None; + } + } + + /// VBlank number of the latest full captured frame, including failed frames. + /// None means capture is disabled or no full frame has reached VBlank yet. + pub fn captured_vblank(&self) -> Option { + self.scanline_capture + .as_ref() + .and_then(video::capture::Capture::vblank) + } + + /// Copy the latest captured frame. False means no complete frame is available. + /// Rendering errors are reported here, not as CPU/DMA failures. On errors or + /// false, the output is unchanged. Presenting does not advance time or consume + /// the frame. Writes after capture cannot change its pixels. + pub fn present_frame(&self, output: &mut Framebuffer) -> Result { + match &self.scanline_capture { + Some(capture) => capture.present(output), + None => Ok(false), + } + } + + /// Execute at most one DMA unit. Validate before side effects, then advance + /// device clocks with the CPU paused. Earlier units remain committed on error. + pub(crate) fn step_dma(&mut self) -> Result, DmaError> { + let Some(transfer) = self.io.next_dma()? else { + return Ok(None); + }; + let channel = transfer.channel; + if (POSTFLG..=POSTFLG + 3).contains(&transfer.destination) { + return Err(DmaError::PowerControlDestination { + channel, + address: transfer.destination, + }); + } + if transfer.source < 0x0200_0000 { + return Err(DmaError::UnsupportedSource { + channel, + address: transfer.source, + }); + } + if (DMA_BASE..=DMA_END).contains(&transfer.destination) { + return Err(DmaError::RegisterDestination { + channel, + address: transfer.destination, + }); + } + let map_error = |error| DmaError::Memory { channel, error }; + // Reads have no side effects. A failed destination leaves the unit pending. + let value = match transfer.width { + AccessWidth::Halfword => { + self.write_index::<2>(transfer.destination) + .map_err(map_error)?; + u32::from(self.read16(transfer.source).map_err(map_error)?) + } + AccessWidth::Word => { + self.write_index::<4>(transfer.destination) + .map_err(map_error)?; + self.read32(transfer.source).map_err(map_error)? + } + AccessWidth::Byte => unreachable!("DMA is never byte-wide"), + }; + let kind = if transfer.first { + AccessKind::NonSequential + } else { + AccessKind::Sequential + }; + let timing = StepTiming { + code_cycles: 0, + data_cycles: bus_cycles(self.waitcnt(), transfer.source, transfer.width, kind) + + bus_cycles(self.waitcnt(), transfer.destination, transfer.width, kind), + internal_cycles: if transfer.first { 2 } else { 0 }, + idle_cycles: 0, + }; + match transfer.width { + AccessWidth::Halfword => self.write16(transfer.destination, value as u16), + AccessWidth::Word => self.write32(transfer.destination, value), + AccessWidth::Byte => unreachable!("DMA is never byte-wide"), + } + .map_err(map_error)?; + // Keep this channel active during its cycles: another edge must not + // queue a second block while the current block is still transferring. + self.advance_cycles(timing.total()); + self.io.complete_dma_unit(channel); + Ok(Some((channel, timing))) + } + + /// Whether HALT is waiting for IE & IF to become nonzero. + /// IME and the CPU's interrupt mask do not affect this state. + pub fn halted(&self) -> bool { + self.io.halted() + } + + pub(crate) fn next_event_cycles(&self) -> u32 { + self.io.next_event_cycles() + } + + pub(crate) fn begin_cpu_access(&mut self, pc: u32) { + debug_assert!(self.cpu_access.is_none()); + self.cpu_access = Some(pc); + } + + pub(crate) fn end_cpu_access(&mut self) { + self.cpu_access = None; + } + + fn can_write_power_control(&self) -> bool { + // Bare memory writes are host/debug setup. CPU code must execute in BIOS. + self.cpu_access.is_none_or(|pc| pc < BIOS_SIZE as u32) + } + + /// Current scanline position and frame/VBlank event counters. + pub fn display_position(&self) -> DisplayPosition { + self.io.display_position() + } + + /// The GBA IRQ line after IE/IF/IME gating, before the CPU's interrupt mask. + pub fn irq_pending(&self) -> bool { + self.io.irq_pending() + } + + /// Replace the current pressed-button state. This does not advance time. + /// KEYCNT and keypad interrupts are not implemented yet. + pub fn set_buttons(&mut self, buttons: Buttons) { + self.io.set_buttons(buttons); + } + + /// Render a Mode 0–5 snapshot. No emulated cycles are consumed. + /// Uses programmed affine origins, geometric window bounds, and screen-grid mosaic. + /// Ignores captured history. Use present_frame for latched scanline state. + pub fn render_frame(&self, frame: &mut Framebuffer) -> Result<(), VideoError> { + video::render( + self.io.video_settings(false), + &self.video_ram, + &self.palette_ram, + &self.oam, + frame, + ) + } + + /// Current Game Pak wait-state control. Prefetch is not implemented. + pub fn waitcnt(&self) -> u16 { + self.io.waitcnt() + } + + pub(crate) fn begin_data_timing(&self) { + debug_assert!(self.data_timing.get().is_none()); + self.data_timing.set(Some(DataTiming::default())); + } + + pub(crate) fn end_data_timing(&self) -> u32 { + self.data_timing.take().map_or(0, |trace| trace.cycles) + } + + fn record_access(&self, address: u32, width: AccessWidth) { + if let Some(mut trace) = self.data_timing.get() { + trace.access(self.waitcnt(), address, width); + self.data_timing.set(Some(trace)); + } + } + + pub fn read8(&self, address: u32) -> Result { + let value = self.read_byte(address)?; + self.record_access(address, AccessWidth::Byte); + Ok(value) + } + + // Multi-byte reads use this helper to avoid charging each byte as a bus access. + fn read_byte(&self, address: u32) -> Result { + match address >> 24 { + 0x00 => self + .bios + .as_ref() + .and_then(|bios| bios.get(address as usize)) + .copied() + .ok_or(MemoryError::Unmapped(address)), + 0x02 => Ok(self.external_ram[(address & 0x3ffff) as usize]), + 0x03 => Ok(self.internal_ram[(address & 0x7fff) as usize]), + 0x04 => self.io.read8(address).ok_or(MemoryError::Unmapped(address)), + 0x05 => Ok(self.palette_ram[(address & 0x3ff) as usize]), + 0x06 => Ok(self.video_ram[vram_index(address)]), + 0x07 => Ok(self.oam[(address & 0x3ff) as usize]), + 0x08..=0x0d => self + .rom + .get((address & 0x01ff_ffff) as usize) + .copied() + .ok_or(MemoryError::Unmapped(address)), + _ => Err(MemoryError::Unmapped(address)), + } + } + + pub fn write8(&mut self, address: u32, value: u8) -> Result<(), MemoryError> { + self.write_aligned(address, [value]) + } + + pub fn write16(&mut self, address: u32, value: u16) -> Result<(), MemoryError> { + self.write_aligned(address, value.to_le_bytes()) + } + + pub fn write32(&mut self, address: u32, value: u32) -> Result<(), MemoryError> { + self.write_aligned(address, value.to_le_bytes()) + } + + // Validate the complete access before any RAM or I/O side effects. + // Timer reload bytes precede control bytes in a combined 32-bit write. + fn write_aligned( + &mut self, + address: u32, + bytes: [u8; N], + ) -> Result<(), MemoryError> { + let index = self.validate_write(address, &bytes)?; + self.record_access( + address, + match N { + 1 => AccessWidth::Byte, + 2 => AccessWidth::Halfword, + 4 => AccessWidth::Word, + _ => unreachable!("unsupported bus width"), + }, + ); + if address >> 24 == 0x04 { + for (offset, byte) in bytes.into_iter().enumerate() { + let byte_address = address + offset as u32; + if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() { + continue; // BIOS-only CPU writes; ignored elsewhere, including STOP requests. + } + self.io.write8(byte_address, byte); + } + return Ok(()); + } + if N == 1 && address >> 24 == 0x07 { + return Ok(()); // OAM ignores byte writes; halfword/word writes remain valid. + } + if N == 1 && matches!(address >> 24, 0x05 | 0x06) { + let bg_limit = if (3..=5).contains(&(self.io.display_control() & 7)) { + 0x14000 + } else { + 0x10000 + }; + // Byte writes to OBJ VRAM are ignored. BG/palette writes duplicate + // the byte into both halves of the aligned 16-bit location. + if address >> 24 == 0x06 && index >= bg_limit { + return Ok(()); + } + let ram = if address >> 24 == 0x05 { + &mut self.palette_ram + } else { + &mut self.video_ram + }; + let aligned = index & !1; + ram[aligned..aligned + 2].fill(bytes[0]); + return Ok(()); + } + let ram = match address >> 24 { + 0x02 => &mut self.external_ram, + 0x03 => &mut self.internal_ram, + 0x05 => &mut self.palette_ram, + 0x06 => &mut self.video_ram, + 0x07 => &mut self.oam, + _ => unreachable!("validated writable region"), + }; + ram[index..index + N].copy_from_slice(&bytes); + Ok(()) + } + + fn validate_write( + &self, + address: u32, + bytes: &[u8; N], + ) -> Result { + let index = self.write_index::(address)?; + if self.can_write_power_control() { + for (offset, byte) in bytes.iter().enumerate() { + if address + offset as u32 == HALTCNT && byte & 0x80 != 0 { + return Err(MemoryError::UnsupportedStop); + } + } + } + Ok(index) + } + + fn write_index(&self, address: u32) -> Result { + if address & (N as u32 - 1) != 0 { + return Err(MemoryError::Unaligned(address)); + } + match address >> 24 { + 0x00 if self.bios.is_some() && address < BIOS_SIZE as u32 => { + Err(MemoryError::ReadOnly(address)) + } + 0x02 => Ok((address & 0x3ffff) as usize), + 0x03 => Ok((address & 0x7fff) as usize), + 0x05 => Ok((address & 0x3ff) as usize), + 0x06 => Ok(vram_index(address)), + 0x07 => Ok((address & 0x3ff) as usize), + 0x04 => { + for offset in 0..N as u32 { + if !Io::mapped(address + offset) { + return Err(MemoryError::Unmapped(address + offset)); + } + } + Ok(0) // I/O does not use a RAM index. + } + 0x08..=0x0d => Err(MemoryError::ReadOnly(address)), + _ => Err(MemoryError::Unmapped(address)), + } + } + + /// Validate a batch before any RAM or I/O writes. Diagnostic errors cannot + /// leave a partial block store. This is not a model of hardware data aborts. + /// Mapped I/O reads currently have no side effects; writes cannot fail after validation. + pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> { + for &(address, value) in writes { + self.validate_write(address, &value.to_le_bytes())?; + } + for &(address, value) in writes { + // The map cannot change between validation and these writes. + self.write32(address, value)?; + } + Ok(()) + } + + pub fn read16(&self, address: u32) -> Result { + if address & 1 != 0 { + return Err(MemoryError::Unaligned(address)); + } + let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]); + self.record_access(address, AccessWidth::Halfword); + Ok(value) + } + + /// The bus requires alignment; the CPU applies ARM7TDMI load rotation. + pub fn read32(&self, address: u32) -> Result { + if address & 3 != 0 { + return Err(MemoryError::Unaligned(address)); + } + let value = u32::from_le_bytes([ + self.read_byte(address)?, + self.read_byte(address + 1)?, + self.read_byte(address + 2)?, + self.read_byte(address + 3)?, + ]); + self.record_access(address, AccessWidth::Word); + Ok(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display::CYCLES_PER_LINE, + io::{DISPSTAT, IF}, + }; + + #[test] + fn affine_state_tracks_without_capture_and_survives_failed_batch_writes() { + use crate::io::{BG2PA, BG2PB, BG2PD, BG2X, DISPCNT}; + let mut bulk = Memory::new(vec![]).unwrap(); + let mut captured = Memory::new(vec![]).unwrap(); + captured.set_scanline_rendering(true); + for m in [&mut bulk, &mut captured] { + m.write16(DISPCNT, 0x403).unwrap(); + m.write16(BG2PA, 256).unwrap(); + m.write16(BG2PB, (-181i16) as u16).unwrap(); + m.write16(BG2PD, 91).unwrap(); + m.write32(BG2X, 12345).unwrap(); + } + for cycles in [0, 1006, 226, 7319, 280896, 1232 * 317] { + for m in [&mut bulk, &mut captured] { + m.advance_cycles(cycles); + } + assert_eq!( + bulk.io.video_settings(true).backgrounds[2].affine, + captured.io.video_settings(true).backgrounds[2].affine + ); + } + let before = bulk.io.video_settings(true).backgrounds[2].affine; + assert!(bulk.write_words(&[(BG2X, 0), (0x0400_0058, 0)]).is_err()); + assert_eq!(bulk.io.video_settings(true).backgrounds[2].affine, before); + bulk.set_scanline_rendering(true); + bulk.set_scanline_rendering(false); + assert_eq!(bulk.io.video_settings(true).backgrounds[2].affine, before); + } + + #[test] + fn window_flags_match_batched_and_captured_clocks_and_survive_failed_writes() { + use crate::io::{WIN0V, WIN1V}; + let mut bulk = Memory::new(vec![]).unwrap(); + let mut captured = Memory::new(vec![]).unwrap(); + captured.set_scanline_rendering(true); + for (cycles, first, second) in [ + (1006, 0x0010, 0xc808), + (226, 0x0010, 0xc808), + (1232 * 5, 0x0508, 0x0014), + (280896 * 3, 0xffff, 0xc808), + (1232 * 500 + 100, 0x00ff, 0xff00), + ] { + for m in [&mut bulk, &mut captured] { + m.write16(WIN0V, first).unwrap(); + m.write16(WIN1V, second).unwrap(); + m.advance_cycles(cycles); + } + assert_eq!( + bulk.io.video_settings(true).vertical_windows, + captured.io.video_settings(true).vertical_windows + ); + } + let before = bulk.io.clone(); + assert!(bulk.write_words(&[(WIN0V, 0), (0x0400_0058, 0)]).is_err()); + assert_eq!(bulk.io, before); + bulk.set_scanline_rendering(true); + bulk.set_scanline_rendering(false); + assert_eq!(bulk.io, before); + } + + #[test] + fn mosaic_phase_matches_batched_capture_and_survives_writes_and_capture_toggles() { + use crate::io::{BG2CNT, BG2PD, DISPCNT, MOSAIC}; + let mut bulk = Memory::new(vec![]).unwrap(); + let mut captured = Memory::new(vec![]).unwrap(); + captured.set_scanline_rendering(true); + for m in [&mut bulk, &mut captured] { + m.write16(DISPCNT, 0x403).unwrap(); + m.write16(BG2CNT, 0x40).unwrap(); + m.write16(BG2PD, 91).unwrap(); + } + for (cycles, sizes) in [ + (6 * CYCLES_PER_LINE, 0x7070), + (9 * CYCLES_PER_LINE, 0x1010), + (1006, 0x3010), + (226, 0x0030), + (280896 * 3 + 1232 * 5, 0x9030), + ] { + for m in [&mut bulk, &mut captured] { + m.write16(MOSAIC, sizes).unwrap(); + m.advance_cycles(cycles); + } + assert_eq!(bulk.io, captured.io); + } + let before = bulk.io.clone(); + assert!(bulk.write_words(&[(MOSAIC, 0), (0x0400_0058, 0)]).is_err()); + assert_eq!(bulk.io, before); + bulk.set_scanline_rendering(true); + bulk.set_scanline_rendering(false); + assert_eq!(bulk.io, before); + let counters = bulk.io.video_settings(true).vertical_mosaic; + bulk.write16(MOSAIC, 0).unwrap(); + bulk.advance_cycles(0); + assert_eq!(bulk.io.video_settings(true).vertical_mosaic, counters); + } + + #[test] + fn horizontal_history_matches_capture_clock_splits_and_survives_failed_writes() { + use crate::io::WIN0H; + let mut bulk = Memory::new(vec![]).unwrap(); + let mut captured = Memory::new(vec![]).unwrap(); + captured.set_scanline_rendering(true); + for (cycles, bounds) in [ + (81, 0x0a50), + (3, 0x1450), + (922, 0x2828), + (14, 0xff0a), + (1, 0xff0a), + (211, 0xff0a), + (280896 * 3 + 100, 0xf014), + ] { + for m in [&mut bulk, &mut captured] { + m.write32(WIN0H, bounds | ((bounds ^ 0x371b) << 16)) + .unwrap(); + m.advance_cycles(cycles); + } + assert_eq!(bulk.io, captured.io); + } + let before = bulk.io.clone(); + assert!(bulk.write_words(&[(WIN0H, 0), (0x0400_0058, 0)]).is_err()); + assert_eq!(bulk.io, before); + bulk.advance_cycles(0); + bulk.set_scanline_rendering(true); + bulk.set_scanline_rendering(false); + let mut snapshot = Framebuffer::default(); + bulk.render_frame(&mut snapshot).unwrap(); + assert_eq!(bulk.io, before); + } + + #[test] + fn failed_batch_does_not_change_comparison_or_latch_match_irq() { + let mut bus = Memory::new(vec![]).unwrap(); + bus.advance_cycles(7 * CYCLES_PER_LINE); + let before = bus.display_position(); + // This batch is no longer reachable with one CPU STM: the mapped + // display register area now exceeds its sixteen-word transfer limit. + assert_eq!( + bus.write_words(&[(DISPSTAT, 0x720), (0x0400_0058, 0)]), + Err(MemoryError::Unmapped(0x0400_0058)) + ); + assert_eq!(bus.display_position(), before); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 0); + assert_eq!(bus.read16(IF).unwrap(), 0); + } +} diff --git a/tests/memory.rs b/tests/memory.rs new file mode 100644 index 0000000..5e8b5a2 --- /dev/null +++ b/tests/memory.rs @@ -0,0 +1,87 @@ +use gba_rust::memory::{Memory, MemoryError, ROM_START}; + +#[test] +fn halfword_and_word_writes_are_little_endian() { + let mut memory = Memory::new(vec![]).unwrap(); + for address in [0x0200_0000, 0x0300_0000] { + memory.write32(address, 0x1234_5678).unwrap(); + assert_eq!(memory.read8(address).unwrap(), 0x78); + assert_eq!(memory.read16(address).unwrap(), 0x5678); + assert_eq!(memory.read16(address + 2).unwrap(), 0x1234); + memory.write16(address + 2, 0xabcd).unwrap(); + assert_eq!(memory.read32(address).unwrap(), 0xabcd_5678); + } +} + +#[test] +fn multi_byte_writes_respect_ram_mirrors_and_region_ends() { + let mut memory = Memory::new(vec![]).unwrap(); + for (base, size, end) in [ + (0x0200_0000, 0x40000, 0x02ff_ffff), + (0x0300_0000, 0x8000, 0x03ff_ffff), + ] { + memory.write32(end - 3, 0xdead_beef).unwrap(); + assert_eq!(memory.read32(base + size - 4).unwrap(), 0xdead_beef); + memory.write16(base + size, 0x1234).unwrap(); + assert_eq!(memory.read16(base).unwrap(), 0x1234); + } +} + +#[test] +fn unaligned_bus_writes_fail_without_modifying_memory() { + let mut memory = Memory::new(vec![]).unwrap(); + let address = 0x0200_0000; + memory.write32(address, 0xdead_beef).unwrap(); + assert_eq!( + memory.write16(address + 1, 0), + Err(MemoryError::Unaligned(address + 1)) + ); + for offset in 1..4 { + assert_eq!( + memory.write32(address + offset, 0), + Err(MemoryError::Unaligned(address + offset)) + ); + } + assert_eq!(memory.read32(address).unwrap(), 0xdead_beef); + assert_eq!( + memory.read16(address + 1), + Err(MemoryError::Unaligned(address + 1)) + ); +} + +#[test] +fn rom_and_unmapped_writes_return_errors_without_partial_changes() { + let mut memory = Memory::new(vec![0x12, 0x34, 0x56, 0x78]).unwrap(); + assert_eq!( + memory.write16(ROM_START, 0), + Err(MemoryError::ReadOnly(ROM_START)) + ); + assert_eq!( + memory.write32(ROM_START, 0), + Err(MemoryError::ReadOnly(ROM_START)) + ); + assert_eq!(memory.read32(ROM_START).unwrap(), 0x7856_3412); + for address in [0, 0x0400_0058, 0xffff_fffc] { + assert_eq!( + memory.write32(address, 0), + Err(MemoryError::Unmapped(address)) + ); + assert_eq!( + memory.write16(address, 0), + Err(MemoryError::Unmapped(address)) + ); + } +} + +#[test] +fn truncated_rom_halfword_read_returns_an_error() { + let memory = Memory::new(vec![0x12]).unwrap(); + assert_eq!( + memory.read16(ROM_START), + Err(MemoryError::Unmapped(ROM_START + 1)) + ); + assert_eq!( + memory.read16(0xffff_fffe), + Err(MemoryError::Unmapped(0xffff_fffe)) + ); +} diff --git a/tests/timers.rs b/tests/timers.rs new file mode 100644 index 0000000..600dc5d --- /dev/null +++ b/tests/timers.rs @@ -0,0 +1,360 @@ +use gba_rust::{ + io::{IE, IF, IME, TIMER_BASE}, + memory::{Memory, MemoryError}, +}; + +fn memory() -> Memory { + Memory::new(vec![]).unwrap() +} + +#[test] +fn io_reset_values_masks_and_unused_bytes() { + let mut bus = memory(); + for address in (TIMER_BASE..TIMER_BASE + 16) + .chain(IE..IE + 4) + .chain(IME..IME + 4) + { + assert_eq!(bus.read8(address).unwrap(), 0); + } + bus.write16(IE, 0xffff).unwrap(); + assert_eq!(bus.read16(IE).unwrap(), 0x3fff); + bus.write32(IME, u32::MAX).unwrap(); + assert_eq!(bus.read32(IME).unwrap(), 1); + bus.write32(IME, 0xffff_fffe).unwrap(); + assert_eq!(bus.read32(IME).unwrap(), 0); + for index in 0..4 { + let address = TIMER_BASE + index * 4 + 2; + bus.write16(address, 0xffff).unwrap(); + assert_eq!( + bus.read16(address).unwrap(), + if index == 0 { 0xc3 } else { 0xc7 } + ); + } +} + +#[test] +fn timer_data_reads_counter_not_reload_and_start_loads_reload() { + let mut bus = memory(); + bus.write16(TIMER_BASE, 0x1234).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0); + bus.advance_cycles(100); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0); + bus.write16(TIMER_BASE + 2, 0x80).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234); + bus.advance_cycles(5); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239); + bus.write16(TIMER_BASE, 0xabcd).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239); + bus.write16(TIMER_BASE + 2, 0x80).unwrap(); // No rising enable edge + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239); + bus.write16(TIMER_BASE + 2, 0).unwrap(); + bus.advance_cycles(100); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1239); + bus.write16(TIMER_BASE + 2, 0x80).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xabcd); +} + +#[test] +fn combined_word_write_uses_new_reload_before_enabling() { + let mut bus = memory(); + for index in 0..4 { + let address = TIMER_BASE + index * 4; + bus.write32(address, 0x0080_abcd).unwrap(); + assert_eq!(bus.read32(address).unwrap(), 0x0080_abcd); + } + bus.advance_cycles(1); + for index in 0..4 { + assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xabce); + } +} + +#[test] +fn reload_byte_writes_merge_with_hidden_latch_not_counter() { + let mut bus = memory(); + bus.write16(TIMER_BASE, 0x1234).unwrap(); + bus.write8(TIMER_BASE, 0x56).unwrap(); + bus.write8(TIMER_BASE + 2, 0x80).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1256); + bus.advance_cycles(1000); + bus.write8(TIMER_BASE + 1, 0xab).unwrap(); + bus.write8(TIMER_BASE + 2, 0).unwrap(); + bus.write8(TIMER_BASE + 2, 0x80).unwrap(); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xab56); +} + +#[test] +fn all_prescalers_retain_fractional_cycles_between_advances() { + for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() { + let mut bus = memory(); + bus.write32(TIMER_BASE, ((0x80 | selection as u32) << 16) | 123) + .unwrap(); + bus.advance_cycles(divisor - 1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 123); + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 124); + bus.advance_cycles(divisor * 3 + divisor - 1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 127); + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 128); + } +} + +#[test] +fn clock_source_changes_and_restart_reset_provisional_phase() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x0081_0000).unwrap(); // /64 + bus.advance_cycles(63); + bus.write16(TIMER_BASE + 2, 0xc1).unwrap(); // IRQ change preserves phase + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1); + bus.advance_cycles(63); + bus.write16(TIMER_BASE + 2, 0x82).unwrap(); // /256 resets phase + bus.advance_cycles(255); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 1); + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 2); + bus.write16(TIMER_BASE + 2, 0).unwrap(); + bus.advance_cycles(255); + bus.write16(TIMER_BASE + 2, 0x82).unwrap(); + bus.advance_cycles(255); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0); +} + +#[test] +fn overflow_reloads_and_latches_request_without_delivery_enables() { + let mut bus = memory(); + for index in 0..4 { + bus.write32(TIMER_BASE + index * 4, 0x00c0_fffe).unwrap(); + } + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0x78); + assert!(!bus.irq_pending()); // IE and IME remain zero + for index in 0..4 { + assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe); + } + bus.advance_cycles(5); + assert_eq!(bus.read16(IF).unwrap(), 0x78); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff); +} + +#[test] +fn overflow_uses_updated_reload_without_resetting_current_counter() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap(); + bus.write16(TIMER_BASE, 0x1234).unwrap(); + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff); + bus.advance_cycles(1); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0x1234); + assert_eq!(bus.read16(IF).unwrap(), 8); +} + +#[test] +fn overflow_with_local_irq_disabled_does_not_set_request() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x0080_ffff).unwrap(); + bus.advance_cycles(100); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.write16(TIMER_BASE + 2, 0xc0).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0); // No retroactive request + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 8); +} + +#[test] +fn cascades_count_all_overflows_including_multiple_levels() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap(); // Overflow every 2 cycles + for index in 1..4 { + bus.write32(TIMER_BASE + index * 4, 0x00c7_fffe).unwrap(); // /1024 ignored + } + bus.advance_cycles(15); + assert_eq!(bus.read16(IF).unwrap(), 0x38); + for index in 0..4 { + assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff); + } + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0x78); + for index in 0..4 { + assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xfffe); + } +} + +#[test] +fn disabled_cascade_stage_blocks_downstream_pulses() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x0080_ffff).unwrap(); + bus.write32(TIMER_BASE + 4, 0x0004_ffff).unwrap(); // Disabled timer 1 + bus.write32(TIMER_BASE + 8, 0x00c4_1234).unwrap(); + bus.advance_cycles(100_000); + assert_eq!(bus.read16(TIMER_BASE + 8).unwrap(), 0x1234); + assert_eq!(bus.read16(IF).unwrap(), 0); +} + +#[test] +fn normal_timer_breaks_cascade_dependency_and_timer_zero_ignores_count_up() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x0084_ffff).unwrap(); // Bit 2 ignored + bus.write32(TIMER_BASE + 8, 0x0080_fffe).unwrap(); // Independent timer 2 + bus.write32(TIMER_BASE + 12, 0x00c4_ffff).unwrap(); + bus.advance_cycles(2); + assert_eq!(bus.read16(TIMER_BASE + 2).unwrap(), 0x80); + assert_eq!(bus.read16(TIMER_BASE).unwrap(), 0xffff); + assert_eq!(bus.read16(IF).unwrap(), 0x40); +} + +#[test] +fn bulk_advance_handles_maximum_cycle_count_and_period_one() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + for index in 1..4 { + bus.write32(TIMER_BASE + index * 4, 0x00c4_ffff).unwrap(); + } + bus.advance_cycles(u32::MAX); + assert_eq!(bus.cycles(), u64::from(u32::MAX)); + for index in 0..4 { + assert_eq!(bus.read16(TIMER_BASE + index * 4).unwrap(), 0xffff); + } + assert_eq!(bus.read16(IF).unwrap(), 0x78); + bus.advance_cycles(u32::MAX); + assert_eq!(bus.cycles(), 2 * u64::from(u32::MAX)); +} + +#[test] +fn bulk_advance_matches_independent_cycle_by_cycle_reference() { + // A small literal reference: count prescaler ticks and propagate one pulse + // per cycle. Unlike the implementation, this does not use overflow division. + for selection in 0..4 { + for cascade_mask in 0..8 { + let mut bus = memory(); + let reloads = [0xfffd_u16, 0xfffe, 0xffff, 0xfffc]; + let mut counters = reloads; + let mut phases = [0_u32; 4]; + let mut pending = 0_u16; + for (index, reload) in reloads.into_iter().enumerate() { + let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0; + let control = 0xc0 | selection | if cascade { 4 } else { 0 }; + bus.write32( + TIMER_BASE + index as u32 * 4, + (control << 16) | u32::from(reload), + ) + .unwrap(); + } + for chunk in [0, 1, 17, 63, 1000, 8193] { + bus.advance_cycles(chunk); + for _ in 0..chunk { + let mut overflow = false; + for index in 0..4 { + let cascade = index > 0 && cascade_mask & (1 << (index - 1)) != 0; + let tick = if cascade { + overflow + } else { + phases[index] += 1; + if phases[index] == [1, 64, 256, 1024][selection as usize] { + phases[index] = 0; + true + } else { + false + } + }; + overflow = tick && counters[index] == u16::MAX; + if overflow { + counters[index] = reloads[index]; + pending |= 1 << (index + 3); + } else if tick { + counters[index] += 1; + } + } + } + for (index, counter) in counters.into_iter().enumerate() { + assert_eq!(bus.read16(TIMER_BASE + index as u32 * 4).unwrap(), counter); + } + assert_eq!(bus.read16(IF).unwrap(), pending); + } + } + } +} + +#[test] +fn interrupt_delivery_requires_both_ie_and_ime_without_clearing_if() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.advance_cycles(1); + for (enable, master, expected) in [ + (0, 0, false), + (8, 0, false), + (0, 1, false), + (16, 1, false), + (8, 1, true), + ] { + bus.write16(IE, enable).unwrap(); + bus.write32(IME, master).unwrap(); + assert_eq!(bus.irq_pending(), expected); + assert_eq!(bus.read16(IF).unwrap(), 8); + } +} + +#[test] +fn interrupt_acknowledgement_is_write_one_to_clear_for_all_widths() { + let mut bus = memory(); + for index in 0..4 { + bus.write32(TIMER_BASE + index * 4, 0x00c0_ffff).unwrap(); + } + bus.advance_cycles(1); + bus.write16(IF, 0).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0x78); + bus.write8(IF + 1, 0xff).unwrap(); // High byte must not clear low flags + assert_eq!(bus.read16(IF).unwrap(), 0x78); + bus.write8(IF, 8).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0x70); + bus.write16(IF, 0x20).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0x50); + bus.write32(IE, 0x0010_0078).unwrap(); // IE=0x78, clear only Timer 1 + assert_eq!(bus.read32(IE).unwrap(), 0x0040_0078); + bus.write16(IF, 0xffff).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0x78); // Later events relatch +} + +#[test] +fn high_byte_writes_do_not_reset_control_or_modify_low_enable_bits() { + let mut bus = memory(); + bus.write32(TIMER_BASE, 0x0080_1234).unwrap(); + bus.write8(TIMER_BASE + 3, 0xff).unwrap(); + bus.write8(IE, 8).unwrap(); + bus.write8(IE + 1, 0xff).unwrap(); + assert_eq!(bus.read16(IE).unwrap(), 0x3f08); + bus.write8(IME, 1).unwrap(); + for offset in 1..4 { + bus.write8(IME + offset, 0xff).unwrap(); + } + assert_eq!(bus.read32(IME).unwrap(), 1); + bus.advance_cycles(1); + assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0x0080_1235); +} + +#[test] +fn unknown_io_and_mirrors_stay_unmapped_and_unaligned_writes_have_no_effect() { + let mut bus = memory(); + for address in [0x0400_0058, TIMER_BASE + 16, IE + 12, IME + 4, 0x0401_0100] { + assert_eq!(bus.read8(address), Err(MemoryError::Unmapped(address))); + assert_eq!( + bus.write32(address, u32::MAX), + Err(MemoryError::Unmapped(address)) + ); + } + assert_eq!( + bus.write32(TIMER_BASE + 2, u32::MAX), + Err(MemoryError::Unaligned(TIMER_BASE + 2)) + ); + assert_eq!( + bus.write16(IF + 1, u16::MAX), + Err(MemoryError::Unaligned(IF + 1)) + ); + assert_eq!(bus.read32(TIMER_BASE).unwrap(), 0); + assert_eq!(bus.read32(IE).unwrap(), 0); + assert_eq!(bus.cycles(), 0); +}