From bb6784615a02dd0e9f3e352cd9dbb89e6618b5bf Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 12:55:35 +0700 Subject: [PATCH] feat(machine): schedule display DMA IRQs and HALT Integrate nominal bus timing, device clocks, display events, DMA transfers, keypad state, and interrupt delivery. --- src/display.rs | 132 ++++++++ src/dma.rs | 259 ++++++++++++++ src/input.rs | 42 +++ src/machine.rs | 176 ++++++++++ src/timing.rs | 126 +++++++ tests/display.rs | 449 +++++++++++++++++++++++++ tests/dma.rs | 860 +++++++++++++++++++++++++++++++++++++++++++++++ tests/halt.rs | 687 +++++++++++++++++++++++++++++++++++++ tests/machine.rs | 272 +++++++++++++++ tests/timing.rs | 169 ++++++++++ 10 files changed, 3172 insertions(+) create mode 100644 src/display.rs create mode 100644 src/dma.rs create mode 100644 src/input.rs create mode 100644 src/machine.rs create mode 100644 src/timing.rs create mode 100644 tests/display.rs create mode 100644 tests/dma.rs create mode 100644 tests/halt.rs create mode 100644 tests/machine.rs create mode 100644 tests/timing.rs diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..6072e66 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,132 @@ +//! GBA display clock and status, independent of pixel rendering. +//! Events use supplied clock cycles. Device updates occur at instruction, DMA, +//! or HALT-idle boundaries; display bus contention and IRQ synchronization delays +//! are not modeled. Forced blank does not stop this clock. + +pub const CPU_HZ: u32 = 16_777_216; +pub const CYCLES_PER_LINE: u32 = 1232; +pub const HBLANK_START: u32 = 1006; +pub const VISIBLE_LINES: u32 = 160; +pub const LINES_PER_FRAME: u32 = 228; +pub const CYCLES_PER_FRAME: u32 = CYCLES_PER_LINE * LINES_PER_FRAME; +pub const VBLANK_START: u32 = CYCLES_PER_LINE * VISIBLE_LINES; + +/// Display position after the latest clock advance. Event counters wrap at u64::MAX. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisplayPosition { + pub scanline: u16, + pub line_cycle: u16, + /// Number of completed 228-line frames (increments on entry to line zero). + pub frames: u64, + /// Number of entries to line 160, independent of local interrupt enables. + pub vblanks: u64, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct Display { + phase: u32, + control: u16, + frames: u64, + vblanks: u64, +} + +impl Display { + pub(crate) fn position(&self) -> DisplayPosition { + DisplayPosition { + scanline: (self.phase / CYCLES_PER_LINE) as u16, + line_cycle: (self.phase % CYCLES_PER_LINE) as u16, + frames: self.frames, + vblanks: self.vblanks, + } + } + + pub(crate) fn status(&self) -> u16 { + let position = self.position(); + // Line 227 is in the non-visible region, but the VBlank flag is clear. + let vblank = (160..227).contains(&position.scanline); + let hblank = u32::from(position.line_cycle) >= HBLANK_START; + let matched = position.scanline == self.control >> 8; + self.control | u16::from(vblank) | (u16::from(hblank) << 1) | (u16::from(matched) << 2) + } + + /// Write a DISPSTAT byte. Status bits and unused GBA bits are read-only. + /// Changing the comparator can generate a rising match event immediately. + /// Enabling an IRQ while its status is already set does not synthesize an edge. + pub(crate) fn write_status(&mut self, high: bool, value: u8) -> u16 { + let previously_matched = self.status() & 4 != 0; + let shift = u32::from(high) * 8; + self.control = ((self.control & !(0xff << shift)) | (u16::from(value) << shift)) & 0xff38; + if !previously_matched && self.status() & 4 != 0 && self.control & 0x20 != 0 { + 4 + } else { + 0 + } + } + + /// Next HBlank entry or scanline start. Both are strictly in the future. + pub(crate) fn next_event_cycles(&self) -> u32 { + let cycle = self.phase % CYCLES_PER_LINE; + if cycle < HBLANK_START { + HBLANK_START - cycle + } else { + CYCLES_PER_LINE - cycle + } + } + + /// Display DMA requests are independent of DISPSTAT interrupt enables. + /// Unlike HBlank IRQs, HBlank DMA requests occur only on visible lines. + pub(crate) fn dma_events(&self, cycles: u32) -> (bool, bool) { + let vblank = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START) != 0; + let visible_hblanks = |time: u64| { + let frame = u64::from(CYCLES_PER_FRAME); + let line = u64::from(CYCLES_PER_LINE); + let visible = u64::from(VISIBLE_LINES); + time / frame * visible + + ((time % frame + line - u64::from(HBLANK_START)) / line).min(visible) + }; + let begin = u64::from(self.phase); + let hblank = visible_hblanks(begin + u64::from(cycles)) != visible_hblanks(begin); + (vblank, hblank) + } + + /// Return IRQ request bits crossed during the interval (old time, new time]. + /// Periodic arithmetic handles large batches without skipping events or + /// looping per cycle/scanline. Status derives from the final clock position. + pub(crate) fn advance(&mut self, cycles: u32) -> u16 { + let vblanks = crossings(self.phase, cycles, CYCLES_PER_FRAME, VBLANK_START); + let frames = crossings(self.phase, cycles, CYCLES_PER_FRAME, 0); + let mut requests = 0; + if self.control & 8 != 0 && vblanks != 0 { + requests |= 1; + } + // HBlank status and IRQ events occur on all 228 lines, including VBlank. + if self.control & 0x10 != 0 + && crossings(self.phase, cycles, CYCLES_PER_LINE, HBLANK_START) != 0 + { + requests |= 2; + } + let compare = u32::from(self.control >> 8); + if self.control & 0x20 != 0 + && compare < LINES_PER_FRAME + && crossings( + self.phase, + cycles, + CYCLES_PER_FRAME, + compare * CYCLES_PER_LINE, + ) != 0 + { + requests |= 4; + } + self.phase = + ((u64::from(self.phase) + u64::from(cycles)) % u64::from(CYCLES_PER_FRAME)) as u32; + self.vblanks = self.vblanks.wrapping_add(vblanks); + self.frames = self.frames.wrapping_add(frames); + requests + } +} + +fn crossings(start: u32, elapsed: u32, period: u32, offset: u32) -> u64 { + let shift = u64::from(period - offset); + let begin = u64::from(start) + shift; + (begin + u64::from(elapsed)) / u64::from(period) - begin / u64::from(period) +} diff --git a/src/dma.rs b/src/dma.rs new file mode 100644 index 0000000..c868318 --- /dev/null +++ b/src/dma.rs @@ -0,0 +1,259 @@ +//! Four-channel GBA direct memory access (DMA) controller. +//! Transfers are scheduled one data unit at a time. Startup/resumption delays, +//! open-bus latches, sound FIFO, video capture, and Game Pak DRQ are not modeled. + +use std::{error::Error, fmt}; + +use crate::{memory::MemoryError, timing::AccessWidth}; + +pub const DMA_BASE: u32 = 0x0400_00b0; +pub const DMA_STRIDE: u32 = 12; +pub(crate) const DMA_END: u32 = DMA_BASE + 4 * DMA_STRIDE - 1; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DmaError { + Memory { channel: usize, error: MemoryError }, + UnsupportedControl { channel: usize, control: u16 }, + UnsupportedSource { channel: usize, address: u32 }, + RegisterDestination { channel: usize, address: u32 }, + PowerControlDestination { channel: usize, address: u32 }, +} + +impl fmt::Display for DmaError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Memory { channel, error } => write!(f, "DMA{channel}: {error}"), + Self::UnsupportedControl { channel, control } => { + write!(f, "DMA{channel}: unsupported control {control:#06x}") + } + Self::UnsupportedSource { channel, address } => { + write!(f, "DMA{channel}: unsupported source {address:#010x}") + } + Self::PowerControlDestination { channel, address } => { + write!( + f, + "DMA{channel}: power-control writes are not supported ({address:#010x})" + ) + } + Self::RegisterDestination { channel, address } => { + write!( + f, + "DMA{channel}: writes to DMA registers are not supported ({address:#010x})" + ) + } + } + } +} + +impl Error for DmaError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Memory { error, .. } => Some(error), + _ => None, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +struct Channel { + source: u32, + destination: u32, + count: u16, + control: u16, + current_source: u32, + current_destination: u32, + remaining: u32, + active: bool, + first: bool, +} + +impl Channel { + fn enabled(&self) -> bool { + self.control & 0x8000 != 0 + } + + fn start_timing(&self) -> u16 { + (self.control >> 12) & 3 + } + + fn unsupported(&self) -> bool { + self.start_timing() == 3 || self.control & 0x800 != 0 || self.control & 0x180 == 0x180 + } + + fn width(&self) -> AccessWidth { + if self.control & 0x400 != 0 { + AccessWidth::Word + } else { + AccessWidth::Halfword + } + } + + fn initial_count(&self, index: usize) -> u32 { + if self.count != 0 { + u32::from(self.count) + } else if index == 3 { + 0x1_0000 + } else { + 0x4000 + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct Dma { + channels: [Channel; 4], +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct Transfer { + pub channel: usize, + pub source: u32, + pub destination: u32, + pub width: AccessWidth, + pub first: bool, +} + +impl Dma { + /// Address/count registers are write-only. Zero is a diagnostic placeholder, + /// not hardware open-bus behavior. Only CNT_H has meaningful readback. + pub(crate) fn read8(&self, address: u32) -> u8 { + let offset = address - DMA_BASE; + let channel = &self.channels[(offset / DMA_STRIDE) as usize]; + match offset % DMA_STRIDE { + 10 => channel.control as u8, + 11 => (channel.control >> 8) as u8, + _ => 0, + } + } + + pub(crate) fn write8(&mut self, address: u32, value: u8) { + let offset = address - DMA_BASE; + let index = (offset / DMA_STRIDE) as usize; + let channel = &mut self.channels[index]; + let register_byte = offset % DMA_STRIDE; + match register_byte { + 0..=3 => { + channel.source = replace_byte(channel.source, register_byte, value) + & if index == 0 { 0x07ff_fffe } else { 0x0fff_fffe }; + } + 4..=7 => { + channel.destination = replace_byte(channel.destination, register_byte - 4, value) + & if index == 3 { 0x0fff_fffe } else { 0x07ff_fffe }; + } + 8..=9 => { + channel.count = replace_byte(u32::from(channel.count), register_byte - 8, value) + as u16 + & if index == 3 { 0xffff } else { 0x3fff }; + } + 10..=11 => { + let was_enabled = channel.enabled(); + channel.control = + replace_byte(u32::from(channel.control), register_byte - 10, value) as u16 + & if index == 3 { 0xffe0 } else { 0xf7e0 }; + if !channel.enabled() { + channel.active = false; + } else if !was_enabled { + let mask = !(channel.width().bytes() - 1); + channel.current_source = channel.source & mask; + channel.current_destination = channel.destination & mask; + channel.remaining = channel.initial_count(index); + channel.first = true; + channel.active = channel.start_timing() == 0; + } + } + _ => unreachable!("DMA register byte"), + } + } + + /// Blanking requests are edges, not queued jobs. An active channel ignores + /// further edges until its current block completes. + pub(crate) fn trigger(&mut self, vblank: bool, hblank: bool) { + for channel in &mut self.channels { + if channel.enabled() + && !channel.active + && ((vblank && channel.start_timing() == 1) + || (hblank && channel.start_timing() == 2)) + { + channel.active = true; + } + } + } + + /// Select the highest-priority ready channel without changing its state. + /// Unsupported enabled modes report a diagnostic rather than silently waiting. + pub(crate) fn next(&self) -> Result, DmaError> { + for (index, channel) in self.channels.iter().enumerate() { + if !channel.enabled() { + continue; + } + if channel.unsupported() { + return Err(DmaError::UnsupportedControl { + channel: index, + control: channel.control, + }); + } + if channel.active { + return Ok(Some(Transfer { + channel: index, + source: channel.current_source, + destination: channel.current_destination, + width: channel.width(), + first: channel.first, + })); + } + } + Ok(None) + } + + /// Commit one successful data unit after its bus cycles. Return completion IRQ bits. + pub(crate) fn complete_unit(&mut self, index: usize) -> u16 { + let channel = &mut self.channels[index]; + let width = channel.width().bytes(); + // The Game Pak source counter increments regardless of source control. + let source_mode = if (0x08..=0x0d).contains(&(channel.current_source >> 24)) { + 0 + } else { + (channel.control >> 7) & 3 + }; + channel.current_source = advance_address(channel.current_source, width, source_mode); + channel.current_destination = advance_address( + channel.current_destination, + width, + (channel.control >> 5) & 3, + ); + channel.remaining -= 1; + channel.first = false; + if channel.remaining != 0 { + return 0; + } + channel.active = false; + if channel.control & 0x200 != 0 && channel.start_timing() != 0 { + channel.remaining = channel.initial_count(index); + channel.first = true; + if channel.control & 0x60 == 0x60 { + channel.current_destination = channel.destination & !(width - 1); + } + } else { + channel.control &= !0x8000; + } + if channel.control & 0x4000 != 0 { + 1 << (8 + index) + } else { + 0 + } + } +} + +fn advance_address(address: u32, width: u32, mode: u16) -> u32 { + match mode { + 0 | 3 => address.wrapping_add(width), + 1 => address.wrapping_sub(width), + 2 => address, + _ => unreachable!("two-bit address control"), + } +} + +fn replace_byte(previous: u32, byte: u32, value: u8) -> u32 { + let shift = byte * 8; + (previous & !(0xff << shift)) | (u32::from(value) << shift) +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..a540971 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,42 @@ +//! Platform-independent GBA button state. Bits are pressed-high here; +//! the KEYINPUT register exposes their active-low hardware representation. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum Button { + A = 1 << 0, + B = 1 << 1, + Select = 1 << 2, + Start = 1 << 3, + Right = 1 << 4, + Left = 1 << 5, + Up = 1 << 6, + Down = 1 << 7, + R = 1 << 8, + L = 1 << 9, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Buttons(u16); + +impl Buttons { + pub fn from_bits(bits: u16) -> Self { + Self(bits & 0x03ff) + } + + pub fn bits(self) -> u16 { + self.0 + } + + pub fn with(self, button: Button, pressed: bool) -> Self { + Self(if pressed { + self.0 | button as u16 + } else { + self.0 & !(button as u16) + }) + } + + pub(crate) fn keyinput(self) -> u16 { + !self.0 & 0x03ff + } +} diff --git a/src/machine.rs b/src/machine.rs new file mode 100644 index 0000000..334b8e8 --- /dev/null +++ b/src/machine.rs @@ -0,0 +1,176 @@ +//! CPU/device integration with nominal ARM7 instruction and GBA bus costs. +//! Device clocks update at instruction, DMA-unit, or HALT-idle boundaries, not per bus access. + +use std::{error::Error, fmt}; + +use crate::{ + cpu::{Cpu, CpuError}, + dma::DmaError, + memory::Memory, + timing::StepTiming, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepKind { + Instruction, + IrqEntry, + Dma { + channel: usize, + }, + /// Advance device clocks to the next event while the CPU stays paused. + HaltIdle, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MachineError { + Cpu(CpuError), + Dma(DmaError), +} + +impl fmt::Display for MachineError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Cpu(error) => write!(f, "{error}"), + Self::Dma(error) => write!(f, "{error}"), + } + } +} + +impl Error for MachineError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Cpu(error) => Some(error), + Self::Dma(error) => Some(error), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FrameRunError { + Cpu(CpuError), + Dma(DmaError), + StepLimit(usize), +} + +impl fmt::Display for FrameRunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Cpu(error) => write!(f, "{error}"), + Self::Dma(error) => write!(f, "{error}"), + Self::StepLimit(limit) => write!(f, "no VBlank entry within {limit} machine steps"), + } + } +} + +impl Error for FrameRunError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Cpu(error) => Some(error), + Self::Dma(error) => Some(error), + Self::StepLimit(_) => None, + } + } +} + +/// Owns the CPU and memory bus. Use this instead of Cpu::step to advance device +/// clocks, execute DMA, honor HALT, and deliver interrupts. No audio or STOP yet. +pub struct Machine { + cpu: Cpu, + memory: Memory, + last_timing: StepTiming, +} + +impl Machine { + pub fn new(cpu: Cpu, memory: Memory) -> Self { + Self { + cpu, + memory, + last_timing: StepTiming::default(), + } + } + + pub fn cpu(&self) -> &Cpu { + &self.cpu + } + + pub fn memory(&self) -> &Memory { + &self.memory + } + + /// Host setup and inspection. Bus writes do not automatically advance time. + pub fn memory_mut(&mut self) -> &mut Memory { + &mut self.memory + } + + pub fn halted(&self) -> bool { + self.memory.halted() + } + + pub fn cycles(&self) -> u64 { + self.memory.cycles() + } + + /// Execute through the next entry to scanline 160, even when already in VBlank. + /// Stops at the first instruction, DMA-unit, or HALT-idle boundary after the event. + /// DMA requested by that event may still be pending. Prior successful + /// steps remain committed on error or limit exhaustion. Zero steps always fails. + pub fn run_until_vblank(&mut self, max_steps: usize) -> Result { + let previous = self.memory.display_position().vblanks; + for steps in 1..=max_steps { + self.step().map_err(|error| match error { + MachineError::Cpu(error) => FrameRunError::Cpu(error), + MachineError::Dma(error) => FrameRunError::Dma(error), + })?; + if self.memory.display_position().vblanks != previous { + return Ok(steps); + } + } + Err(FrameRunError::StepLimit(max_steps)) + } + + /// Cost of the last successful step. Failed steps leave this unchanged. + pub fn last_timing(&self) -> StepTiming { + self.last_timing + } + + /// Service one ready DMA unit before sampling IRQ or executing CPU code. + /// Lower channel numbers take priority; higher-priority requests can preempt + /// between units. The CPU and its registers remain paused throughout DMA. + /// If HALT is still waiting, advance to the next display edge or timer overflow. + /// Wake-up ignores IME and CPSR.I; IRQ delivery still respects both masks. + /// Sample IRQ before executing the next instruction. GBA devices do not + /// generate FIQ. IRQ entry consumes a separate step and does not clear IF. + /// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event. + /// CPU writes take effect before this bulk device update. Within-instruction + /// register timing, prefetch, and IRQ synchronization delays are not modeled. + /// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step. + /// Row-capture diagnostics are deferred to Memory::present_frame instead. + pub fn step(&mut self) -> Result { + if let Some((channel, timing)) = self.memory.step_dma().map_err(MachineError::Dma)? { + self.last_timing = timing; + return Ok(StepKind::Dma { channel }); + } + if self.memory.halted() { + let timing = StepTiming { + idle_cycles: self.memory.next_event_cycles(), + ..StepTiming::default() + }; + self.memory.advance_cycles(timing.total()); + self.last_timing = timing; + return Ok(StepKind::HaltIdle); + } + let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) { + (StepKind::IrqEntry, self.cpu.exception_timing(&self.memory)) + } else { + ( + StepKind::Instruction, + self.cpu + .step_timed(&mut self.memory) + .map_err(MachineError::Cpu)?, + ) + }; + self.memory.advance_cycles(timing.total()); + self.last_timing = timing; + Ok(kind) + } +} diff --git a/src/timing.rs b/src/timing.rs new file mode 100644 index 0000000..cae2431 --- /dev/null +++ b/src/timing.rs @@ -0,0 +1,126 @@ +//! Nominal GBA bus costs. These calculations do not perform memory accesses. +//! Game Pak prefetch, display-bus contention, and configurable EWRAM timing are not modeled. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccessWidth { + Byte, + Halfword, + Word, +} + +impl AccessWidth { + pub(crate) fn bytes(self) -> u32 { + match self { + Self::Byte => 1, + Self::Halfword => 2, + Self::Word => 4, + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum AccessKind { + NonSequential, + #[default] + Sequential, +} + +/// Timing breakdown for one instruction, exception entry, DMA unit, or HALT batch. +/// DMA has zero code cycles and charges two internal cycles on its first unit. +/// This is a nominal estimate, not a cycle-accurate bus trace. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct StepTiming { + pub code_cycles: u32, + pub data_cycles: u32, + pub internal_cycles: u32, + /// Device-clock cycles while the CPU is halted, without CPU or DMA bus work. + pub idle_cycles: u32, +} + +impl StepTiming { + pub fn total(self) -> u32 { + self.code_cycles + self.data_cycles + self.internal_cycles + self.idle_cycles + } +} + +/// Compute an access cost from a WAITCNT snapshot. This does not validate the +/// address against the memory map. Unmapped regions retain a one-cycle fallback +/// so a branch can finish before the next instruction fetch reports a diagnostic. +/// WAITCNT's prefetch and PHI bits are stored but do not affect this calculation. +pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKind) -> u32 { + match address >> 24 { + 0x02 => { + if width == AccessWidth::Word { + 6 + } else { + 3 + } + } + 0x05 | 0x06 => { + if width == AccessWidth::Word { + 2 + } else { + 1 + } + } + 0x08..=0x0d => { + let window = ((address >> 25) - 4) as usize; + let first_shift = [2, 5, 8][window]; + let second_shift = [4, 7, 10][window]; + let first = 1 + [4, 3, 2, 8][usize::from((waitcnt >> first_shift) & 3)]; + let second = 1 + if waitcnt & (1 << second_shift) != 0 { + 1 + } else { + [2, 4, 8][window] + }; + // The Game Pak bus forces N timing at each 128 KiB boundary. + let sequential = kind == AccessKind::Sequential && address & 0x1fffe != 0; + let cycles = if sequential { second } else { first }; + // Each word is two halfwords. The second halfword always uses S timing. + cycles + + if width == AccessWidth::Word { + second + } else { + 0 + } + } + // BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped. + _ => 1, + } +} + +pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 { + bus_cycles(waitcnt, target, width, AccessKind::NonSequential) + + bus_cycles( + waitcnt, + target.wrapping_add(width.bytes()), + width, + AccessKind::Sequential, + ) + + bus_cycles( + waitcnt, + target.wrapping_add(2 * width.bytes()), + width, + AccessKind::Sequential, + ) +} + +/// A data-only trace. Instruction fetches and branch refills are charged by the +/// CPU timing summary, not here. Actual CPU bus calls supply addresses and widths. +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct DataTiming { + pub cycles: u32, + next: Option, +} + +impl DataTiming { + pub fn access(&mut self, waitcnt: u16, address: u32, width: AccessWidth) { + let kind = if self.next == Some(address) { + AccessKind::Sequential + } else { + AccessKind::NonSequential + }; + self.cycles += bus_cycles(waitcnt, address, width, kind); + self.next = Some(address.wrapping_add(width.bytes())); + } +} diff --git a/tests/display.rs b/tests/display.rs new file mode 100644 index 0000000..5cba467 --- /dev/null +++ b/tests/display.rs @@ -0,0 +1,449 @@ +use gba_rust::{ + cpu::{Cpu, Mode}, + display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, LINES_PER_FRAME, VBLANK_START}, + io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, VCOUNT}, + machine::{FrameRunError, Machine, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, +}; + +fn memory() -> Memory { + Memory::new(vec![]).unwrap() +} + +#[test] +fn display_starts_at_line_zero_with_comparison_match_but_no_irq() { + let bus = memory(); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 4); + assert_eq!(bus.read16(VCOUNT).unwrap(), 0); + assert_eq!(bus.read16(IF).unwrap(), 0); + assert_eq!(bus.display_position().scanline, 0); + assert_eq!(bus.display_position().line_cycle, 0); + assert_eq!(bus.display_position().frames, 0); + assert_eq!(bus.display_position().vblanks, 0); + assert_eq!(bus.cycles(), 0); +} + +#[test] +fn hblank_status_starts_at_1006_not_at_the_last_visible_pixel() { + let mut bus = memory(); + bus.advance_cycles(960); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0); + bus.advance_cycles(45); + assert_eq!(bus.display_position().line_cycle, 1005); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2); + bus.advance_cycles(225); + assert_eq!(bus.read16(VCOUNT).unwrap(), 0); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 2, 2); + bus.advance_cycles(1); + assert_eq!(bus.read16(VCOUNT).unwrap(), 1); + assert_eq!(bus.display_position().line_cycle, 0); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 7, 0); +} + +#[test] +fn vblank_starts_at_160_clears_at_227_and_line_count_wraps_at_228() { + let mut bus = memory(); + bus.advance_cycles(VBLANK_START - 1); + assert_eq!(bus.read16(VCOUNT).unwrap(), 159); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 2); + bus.advance_cycles(1); + assert_eq!(bus.read16(VCOUNT).unwrap(), 160); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 1); + assert_eq!(bus.display_position().vblanks, 1); + bus.advance_cycles(67 * CYCLES_PER_LINE - 1); + assert_eq!(bus.read16(VCOUNT).unwrap(), 226); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 3); + bus.advance_cycles(1); + assert_eq!(bus.read16(VCOUNT).unwrap(), 227); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 3, 0); + assert_eq!(bus.display_position().frames, 0); + bus.advance_cycles(CYCLES_PER_LINE); + assert_eq!(bus.read32(DISPSTAT).unwrap(), 4); // Compare zero matches again + assert_eq!(bus.display_position().frames, 1); + assert_eq!(bus.display_position().vblanks, 1); + assert_eq!(bus.cycles(), 280_896); +} + +#[test] +fn hblank_irq_occurs_on_every_line_including_hidden_lines() { + let mut bus = memory(); + bus.write16(DISPSTAT, 0x10).unwrap(); + for line in 0..LINES_PER_FRAME { + bus.advance_cycles(HBLANK_START - 1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(VCOUNT).unwrap(), line as u16); + assert_eq!(bus.read16(IF).unwrap(), 2); + bus.write16(IF, 2).unwrap(); + bus.advance_cycles(CYCLES_PER_LINE - HBLANK_START); + assert_eq!(bus.read16(IF).unwrap(), 0); + } + assert_eq!(bus.display_position().frames, 1); +} + +#[test] +fn vblank_and_compare_requests_latch_without_ie_or_ime_and_acknowledge_independently() { + let mut bus = memory(); + bus.write16(DISPSTAT, 0xa028).unwrap(); // VBlank + VCount=160 + bus.advance_cycles(VBLANK_START); + assert_eq!(bus.read16(IF).unwrap(), 5); + assert!(!bus.irq_pending()); + bus.write16(IE, 5).unwrap(); + assert!(!bus.irq_pending()); + bus.write32(IME, 1).unwrap(); + assert!(bus.irq_pending()); + bus.write16(IF, 1).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 4); + assert!(bus.irq_pending()); + bus.write16(IF, 4).unwrap(); + assert!(!bus.irq_pending()); + bus.advance_cycles(0); + bus.advance_cycles(10); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(CYCLES_PER_FRAME - 10); + assert_eq!(bus.read16(IF).unwrap(), 5); +} + +#[test] +fn register_masks_read_only_fields_and_word_accesses_are_coherent() { + let mut bus = memory(); + bus.advance_cycles(17 * CYCLES_PER_LINE + HBLANK_START); + bus.write32(DISPSTAT, u32::MAX).unwrap(); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff3a); + assert_eq!(bus.read16(VCOUNT).unwrap(), 17); + assert_eq!(bus.read32(DISPSTAT).unwrap(), 0x0011_ff3a); + bus.write8(VCOUNT, 200).unwrap(); + bus.write8(VCOUNT + 1, 0xff).unwrap(); + assert_eq!(bus.read16(VCOUNT).unwrap(), 17); + bus.write8(DISPSTAT, 0).unwrap(); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 0xff02); + bus.write8(DISPSTAT + 1, 17).unwrap(); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106); + assert_eq!(bus.read8(DISPSTAT).unwrap(), 6); + assert_eq!(bus.read8(DISPSTAT + 1).unwrap(), 17); + assert_eq!(bus.read8(VCOUNT + 1).unwrap(), 0); + assert_eq!(bus.read16(IF).unwrap(), 0); + assert_eq!( + bus.write16(DISPSTAT + 1, 0), + Err(MemoryError::Unaligned(DISPSTAT + 1)) + ); + assert_eq!(bus.read16(DISPSTAT).unwrap(), 0x1106); +} + +#[test] +fn writing_comparator_can_create_one_immediate_rising_match_request() { + let mut bus = memory(); + bus.advance_cycles(7 * CYCLES_PER_LINE); + bus.write16(DISPSTAT, 0x0620).unwrap(); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0); + bus.write8(DISPSTAT + 1, 7).unwrap(); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 4); + assert_eq!(bus.read16(IF).unwrap(), 4); + bus.write16(IF, 4).unwrap(); + bus.write8(DISPSTAT + 1, 7).unwrap(); // Already matches + bus.write8(DISPSTAT, 0).unwrap(); + bus.write8(DISPSTAT, 0x20).unwrap(); // Enabling alone does not create a match edge + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.write8(DISPSTAT + 1, 6).unwrap(); + bus.write8(DISPSTAT + 1, 7).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 4); +} + +#[test] +fn zero_comparator_irq_waits_for_the_next_line_zero_entry() { + let mut bus = memory(); + bus.write16(DISPSTAT, 0x20).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(CYCLES_PER_FRAME - 1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 4); + bus.write16(IF, 4).unwrap(); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 0); +} + +#[test] +fn out_of_range_comparator_values_never_match() { + let mut bus = memory(); + for compare in 228..=255 { + bus.write16(DISPSTAT, (compare << 8) | 0x20).unwrap(); + bus.advance_cycles(CYCLES_PER_FRAME); + assert_eq!(bus.read16(DISPSTAT).unwrap() & 4, 0); + assert_eq!(bus.read16(IF).unwrap(), 0); + } +} + +#[test] +fn enabling_vblank_or_hblank_irq_does_not_replay_an_active_status() { + let mut bus = memory(); + bus.advance_cycles(VBLANK_START); + bus.write16(DISPSTAT, 8).unwrap(); + bus.advance_cycles(0); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(CYCLES_PER_FRAME - 1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 1); + + let mut bus = memory(); + bus.advance_cycles(HBLANK_START); + bus.write16(DISPSTAT, 0x10).unwrap(); + bus.advance_cycles(CYCLES_PER_LINE - 1); + assert_eq!(bus.read16(IF).unwrap(), 0); + bus.advance_cycles(1); + assert_eq!(bus.read16(IF).unwrap(), 2); +} + +#[test] +fn forced_blank_and_display_mode_changes_do_not_stop_or_reset_the_clock() { + let mut bus = memory(); + bus.write16(DISPCNT, 0x80).unwrap(); + bus.write16(DISPSTAT, 0x18).unwrap(); + bus.advance_cycles(VBLANK_START); + let position = bus.display_position(); + assert_eq!(bus.read16(IF).unwrap(), 3); + bus.write16(DISPCNT, 0x403).unwrap(); + assert_eq!(bus.display_position(), position); + bus.advance_cycles(CYCLES_PER_FRAME); + assert_eq!(bus.display_position().vblanks, 2); + assert_eq!(bus.display_position().scanline, 160); +} + +#[test] +fn timer_and_display_events_can_latch_in_the_same_clock_batch() { + let mut bus = memory(); + let reload = 65536 - HBLANK_START; + bus.write32(TIMER_BASE, 0x00c0_0000 | reload).unwrap(); + bus.write16(DISPSTAT, 0x10).unwrap(); + bus.advance_cycles(HBLANK_START); + assert_eq!(bus.read16(IF).unwrap(), 0x0a); + bus.write16(IF, 2).unwrap(); + assert_eq!(bus.read16(IF).unwrap(), 8); +} + +#[test] +fn bulk_clock_advances_match_a_cycle_by_cycle_reference() { + for compare in [0_u16, 1, 160, 226, 227, 255] { + let mut bus = memory(); + bus.write16(DISPSTAT, (compare << 8) | 0x38).unwrap(); + let mut line = 0_u16; + let mut cycle = 0_u16; + let mut frames = 0_u64; + let mut vblanks = 0_u64; + for chunk in [ + 0, + 1, + 959, + 46, + 226, + 17_777, + CYCLES_PER_FRAME * 2 + 99, + 1232, + 77, + ] { + let mut pending = 0; + for _ in 0..chunk { + cycle += 1; + if cycle == 1006 { + pending |= 2; + } + if cycle == 1232 { + cycle = 0; + line += 1; + if line == 228 { + line = 0; + frames += 1; + } + if line == 160 { + vblanks += 1; + pending |= 1; + } + if line == compare { + pending |= 4; + } + } + } + bus.advance_cycles(chunk); + assert_eq!( + bus.read16(IF).unwrap(), + pending, + "compare {compare}, chunk {chunk}" + ); + let flags = u16::from((160..227).contains(&line)) + | (u16::from(cycle >= 1006) << 1) + | (u16::from(line == compare) << 2); + assert_eq!(bus.read16(DISPSTAT).unwrap(), (compare << 8) | 0x38 | flags); + let position = bus.display_position(); + assert_eq!( + ( + position.scanline, + position.line_cycle, + position.frames, + position.vblanks + ), + (line, cycle, frames, vblanks) + ); + bus.write16(IF, 7).unwrap(); + } + } +} + +#[test] +fn maximum_batches_preserve_multi_frame_counts_and_do_not_overflow_u32() { + let mut bus = memory(); + bus.write16(DISPSTAT, 0xa038).unwrap(); + for multiplier in 1..=2_u64 { + bus.advance_cycles(u32::MAX); + let total = multiplier * u64::from(u32::MAX); + let phase = total % u64::from(CYCLES_PER_FRAME); + let position = bus.display_position(); + assert_eq!(position.frames, total / u64::from(CYCLES_PER_FRAME)); + assert_eq!( + position.vblanks, + (total + u64::from(CYCLES_PER_FRAME - VBLANK_START)) / u64::from(CYCLES_PER_FRAME) + ); + assert_eq!( + u64::from(position.scanline), + phase / u64::from(CYCLES_PER_LINE) + ); + assert_eq!( + u64::from(position.line_cycle), + phase % u64::from(CYCLES_PER_LINE) + ); + assert_eq!(bus.read16(IF).unwrap(), 7); + assert_eq!(bus.cycles(), total); + bus.write16(IF, 7).unwrap(); + } +} + +#[test] +fn display_sources_enter_an_original_irq_handler_acknowledge_and_return() { + for (control, source, edge) in [ + (8, 1, VBLANK_START), + (0x10, 2, HBLANK_START), + (0xa020, 4, VBLANK_START), + ] { + let mut bios = vec![0; BIOS_SIZE]; + for (index, instruction) in [0xe1c1_20b2_u32, 0xe280_0001, 0xe25e_f004] + .into_iter() + .enumerate() + { + // STRH r2,[r1,#2] (IF); ADD r0,r0,#1; SUBS pc,lr,#4 + let offset = 0x18 + index * 4; + bios[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + let rom = [ + 0xe3a0_1301_u32, + 0xe281_1c02, + 0xe3a0_2000 | source, + 0xe283_3001, + 0xeaff_fffe, + ] + .into_iter() + .flat_map(u32::to_le_bytes) + .collect(); + let mut bus = Memory::with_bios(rom, bios).unwrap(); + let mut cpu = Cpu::new(ROM_START); + for _ in 0..3 { + cpu.step(&mut bus).unwrap(); + } + bus.write16(DISPSTAT, control).unwrap(); + bus.write16(IE, source as u16).unwrap(); + bus.write32(IME, 1).unwrap(); + bus.advance_cycles(edge - 1); + let mut machine = Machine::new(cpu, bus); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[3], 1); + assert_eq!(machine.memory().read16(IF).unwrap(), source as u16); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().mode(), Mode::Irq); + assert_eq!(machine.cpu().pc(), 0x18); + assert_eq!(machine.cpu().registers()[14], ROM_START + 20); + for _ in 0..3 { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + assert_eq!(machine.cpu().registers()[0], 1); + assert_eq!(machine.cpu().mode(), Mode::System); + assert_eq!(machine.cpu().pc(), ROM_START + 16); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + } +} + +#[test] +fn failed_block_store_does_not_change_window_controls_or_advance_display() { + let rom = [ + 0xe3a0_2301_u32, // MOV r2,#0x04000000 + 0xe282_2048, // ADD r2,r2,#0x48 (WININ/WINOUT) + 0xe3a0_003f, // MOV r0,#0x3f + 0xe3a0_1001, // MOV r1,#1 + 0xe8a2_001f, // STMIA r2!,{r0-r4}; last word is unmapped 0x04000058 + ] + .into_iter() + .flat_map(u32::to_le_bytes) + .collect(); + let mut bus = Memory::new(rom).unwrap(); + let mut cpu = Cpu::new(ROM_START); + for _ in 0..4 { + cpu.step(&mut bus).unwrap(); + } + bus.advance_cycles(7 * CYCLES_PER_LINE); + let before = cpu.clone(); + let position = bus.display_position(); + let mut machine = Machine::new(cpu, bus); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.memory().display_position(), position); + assert_eq!(machine.memory().read32(gba_rust::io::WININ).unwrap(), 0); + assert_eq!(machine.memory().read16(DISPSTAT).unwrap(), 0); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); +} + +#[test] +fn frame_runner_waits_for_next_vblank_event_not_an_already_set_flag() { + let mut bus = memory(); + bus.write32(0x0300_0000, 0xeaff_fffe).unwrap(); // 3-cycle IWRAM self-branch + bus.advance_cycles(VBLANK_START - 2); + let mut machine = Machine::new(Cpu::new(0x0300_0000), bus); + assert_eq!(machine.run_until_vblank(1).unwrap(), 1); + assert_eq!(machine.memory().display_position().line_cycle, 1); + assert_eq!(machine.memory().display_position().vblanks, 1); + let steps = machine + .run_until_vblank((CYCLES_PER_FRAME / 3) as usize) + .unwrap(); + assert_eq!(steps, (CYCLES_PER_FRAME / 3) as usize); + assert_eq!(machine.memory().display_position().line_cycle, 1); + assert_eq!(machine.memory().display_position().vblanks, 2); +} + +#[test] +fn frame_runner_limits_and_cpu_errors_preserve_completed_progress_only() { + let mut bus = memory(); + bus.write32(0x0300_0000, 0xeaff_fffe).unwrap(); + let mut machine = Machine::new(Cpu::new(0x0300_0000), bus); + assert_eq!( + machine.run_until_vblank(0), + Err(FrameRunError::StepLimit(0)) + ); + assert_eq!(machine.cycles(), 0); + assert_eq!( + machine.run_until_vblank(2), + Err(FrameRunError::StepLimit(2)) + ); + assert_eq!(machine.cycles(), 6); + assert_eq!(machine.memory().display_position().line_cycle, 6); + machine + .memory_mut() + .write32(0x0300_0000, 0xf000_0000) + .unwrap(); + let position = machine.memory().display_position(); + assert!(matches!( + machine.run_until_vblank(10), + Err(FrameRunError::Cpu(_)) + )); + assert_eq!(machine.memory().display_position(), position); + assert_eq!(machine.cycles(), 6); +} diff --git a/tests/dma.rs b/tests/dma.rs new file mode 100644 index 0000000..913e1c1 --- /dev/null +++ b/tests/dma.rs @@ -0,0 +1,860 @@ +use gba_rust::{ + cpu::{Cpu, Mode}, + display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START}, + dma::{DmaError, DMA_BASE, DMA_STRIDE}, + io::{DISPCNT, DISPSTAT, IE, IF, IME, TIMER_BASE, WAITCNT}, + machine::{FrameRunError, Machine, MachineError, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, PALETTE_START, ROM_START, VRAM_START}, + timing::{bus_cycles, AccessKind, AccessWidth}, +}; + +const SOURCE: u32 = 0x0200_0000; +const DEST: u32 = 0x0300_0000; + +fn words(values: &[u32]) -> Vec { + values + .iter() + .flat_map(|value| value.to_le_bytes()) + .collect() +} + +fn machine() -> Machine { + Machine::new( + Cpu::new(ROM_START), + Memory::new(words(&[0xeaff_fffe])).unwrap(), + ) +} + +fn base(channel: usize) -> u32 { + DMA_BASE + channel as u32 * DMA_STRIDE +} + +fn configure( + bus: &mut Memory, + channel: usize, + source: u32, + destination: u32, + count: u16, + control: u16, +) { + bus.write32(base(channel), source).unwrap(); + bus.write32(base(channel) + 4, destination).unwrap(); + bus.write32( + base(channel) + 8, + u32::from(count) | (u32::from(control | 0x8000) << 16), + ) + .unwrap(); +} + +fn advance_to(machine: &mut Machine, cycle: u64) { + let elapsed = (cycle - machine.cycles()) as u32; + machine.memory_mut().advance_cycles(elapsed); +} + +fn enabled(machine: &Machine, channel: usize) -> bool { + machine.memory().read16(base(channel) + 10).unwrap() & 0x8000 != 0 +} + +fn units(machine: &mut Machine, channel: usize, count: usize) { + let cpu = machine.cpu().clone(); + for _ in 0..count { + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel }); + assert_eq!(machine.last_timing().code_cycles, 0); + assert_eq!(machine.cpu(), &cpu); + } +} + +#[test] +fn reset_register_layout_masks_and_write_only_readback() { + let mut bus = Memory::new(vec![]).unwrap(); + for channel in 0..4 { + for offset in 0..12 { + assert_eq!(bus.read8(base(channel) + offset).unwrap(), 0); + bus.write8(base(channel) + offset, 0xff).unwrap(); + } + assert_eq!(bus.read32(base(channel)).unwrap(), 0); + assert_eq!(bus.read32(base(channel) + 4).unwrap(), 0); + assert_eq!(bus.read16(base(channel) + 8).unwrap(), 0); + assert_eq!( + bus.read16(base(channel) + 10).unwrap(), + if channel == 3 { 0xffe0 } else { 0xf7e0 } + ); + assert_eq!( + bus.read32(base(channel) + 8).unwrap(), + if channel == 3 { + 0xffe0_0000 + } else { + 0xf7e0_0000 + } + ); + bus.write8(base(channel) + 11, 0).unwrap(); + assert_eq!(bus.read16(base(channel) + 10).unwrap(), 0xe0); + } + assert_eq!( + bus.read8(DMA_BASE - 1), + Err(MemoryError::Unmapped(DMA_BASE - 1)) + ); + assert_eq!( + bus.read8(DMA_BASE + 48), + Err(MemoryError::Unmapped(DMA_BASE + 48)) + ); + assert_eq!( + bus.read16(DMA_BASE + 1), + Err(MemoryError::Unaligned(DMA_BASE + 1)) + ); + assert_eq!( + bus.write32(DMA_BASE + 10, 1), + Err(MemoryError::Unaligned(DMA_BASE + 10)) + ); + assert_eq!( + bus.read16(DMA_BASE + 0x400), + Err(MemoryError::Unmapped(DMA_BASE + 0x400)) + ); +} + +#[test] +fn immediate_halfword_and_word_copies_pause_cpu_until_completion() { + for channel in 0..4 { + for word in [false, true] { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x4433_2211).unwrap(); + machine + .memory_mut() + .write32(SOURCE + 4, 0x8877_6655) + .unwrap(); + configure( + machine.memory_mut(), + channel, + SOURCE, + DEST, + if word { 2 } else { 4 }, + if word { 0x400 } else { 0 }, + ); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0); // Register writes do not execute DMA. + assert_eq!(machine.cycles(), 0); + units(&mut machine, channel, if word { 2 } else { 4 }); + assert!(!enabled(&machine, channel)); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4433_2211); + assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x8877_6655); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.cycles(), if word { 16 } else { 18 }); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + } +} + +#[test] +fn source_and_destination_address_modes_match_reference_copies() { + for word in [false, true] { + let width = if word { 4 } else { 2 }; + for source_mode in 0..3 { + for dest_mode in 0..4 { + let mut machine = machine(); + let mut expected = [0_u32; 9]; + for index in 0..9 { + machine + .memory_mut() + .write32(SOURCE + index * 4, 0x0123_0123 * (index + 1)) + .unwrap(); + } + let mut source = SOURCE + 16; + let mut destination = DEST + 16; + // Read the source and simulate the destination independently of DMA state. + let mut expected_bytes = [0_u8; 36]; + for _ in 0..4 { + for byte in 0..width { + expected_bytes[(destination - DEST + byte) as usize] = + machine.memory().read8(source + byte).unwrap(); + } + source = match source_mode { + 0 => source + width, + 1 => source - width, + _ => source, + }; + destination = match dest_mode { + 0 | 3 => destination + width, + 1 => destination - width, + _ => destination, + }; + } + for (index, chunk) in expected_bytes.chunks_exact(4).enumerate() { + expected[index] = u32::from_le_bytes(chunk.try_into().unwrap()); + } + let control = source_mode << 7 | dest_mode << 5 | if word { 0x400 } else { 0 }; + configure(machine.memory_mut(), 3, SOURCE + 16, DEST + 16, 4, control); + units(&mut machine, 3, 4); + for (index, value) in expected.into_iter().enumerate() { + assert_eq!( + machine.memory().read32(DEST + index as u32 * 4).unwrap(), + value + ); + } + } + } + } +} + +#[test] +fn addresses_mask_channel_bits_and_align_to_transfer_width() { + for channel in 0..4 { + for word in [false, true] { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap(); + let source_high = if channel == 0 { + 0xf800_0000 + } else { + 0xf000_0000 + }; + let dest_high = if channel == 3 { + 0xf000_0000 + } else { + 0xf800_0000 + }; + configure( + machine.memory_mut(), + channel, + SOURCE | source_high | 3, + DEST | dest_high | 3, + 1, + if word { 0x400 } else { 0 }, + ); + units(&mut machine, channel, 1); + assert_eq!( + machine.memory().read32(DEST).unwrap(), + if word { 0xaabb_ccdd } else { 0xaabb_0000 } + ); + } + } +} + +#[test] +fn zero_count_means_channel_maximum_and_count_high_bits_are_masked() { + for (channel, count, expected) in [ + (0, 0, 0x4000), + (1, 0xc000, 0x4000), + (2, 0xc003, 3), + (3, 0, 0x10000), + ] { + let mut machine = machine(); + machine.memory_mut().write16(SOURCE, 0x5a5a).unwrap(); + configure(machine.memory_mut(), channel, SOURCE, DEST, count, 0x140); // Both addresses fixed. + units(&mut machine, channel, expected - 1); + assert!(enabled(&machine, channel)); + units(&mut machine, channel, 1); + assert!(!enabled(&machine, channel)); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x5a5a); + assert_eq!(machine.cycles(), expected as u64 * 4 + 2); + } +} + +#[test] +fn immediate_repeat_completes_once_and_does_not_stall_forever() { + let mut machine = machine(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x200); + units(&mut machine, 0, 1); + assert!(!enabled(&machine, 0)); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); +} + +#[test] +fn address_and_count_latches_change_only_on_enable_edge() { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap(); + machine.memory_mut().write16(SOURCE + 16, 0x3333).unwrap(); + configure(machine.memory_mut(), 2, SOURCE, DEST, 2, 0); + machine.memory_mut().write32(base(2), SOURCE + 16).unwrap(); + machine + .memory_mut() + .write32(base(2) + 4, DEST + 16) + .unwrap(); + machine.memory_mut().write16(base(2) + 8, 1).unwrap(); + machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap(); // Still enabled, not a new edge. + units(&mut machine, 2, 2); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111); + assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0); + machine.memory_mut().write16(base(2) + 10, 0x8000).unwrap(); + units(&mut machine, 2, 1); + assert_eq!(machine.memory().read16(DEST + 16).unwrap(), 0x3333); + assert!(!enabled(&machine, 2)); +} + +#[test] +fn disabling_cancels_pending_dma_and_reenable_reloads_latches() { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0xabcd_1234).unwrap(); + configure(machine.memory_mut(), 1, SOURCE, DEST, 2, 0); + units(&mut machine, 1, 1); + machine.memory_mut().write8(base(1) + 11, 0).unwrap(); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.memory().read16(DEST + 2).unwrap(), 0); + machine.memory_mut().write8(base(1) + 11, 0x80).unwrap(); + units(&mut machine, 1, 2); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0xabcd_1234); +} + +#[test] +fn simultaneous_channels_run_in_priority_order_before_cpu_or_irq() { + let mut machine = machine(); + machine.memory_mut().write16(IE, 0x0f00).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + for channel in (0..4).rev() { + machine + .memory_mut() + .write16(SOURCE + channel as u32 * 2, channel as u16 + 1) + .unwrap(); + configure( + machine.memory_mut(), + channel, + SOURCE + channel as u32 * 2, + DEST, + 1, + 0x4000, + ); + } + for channel in 0..4 { + units(&mut machine, channel, 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), channel as u16 + 1); + assert_eq!( + machine.memory().read16(IF).unwrap(), + ((1 << (channel + 1)) - 1) << 8 + ); + } + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); +} + +#[test] +fn display_request_preempts_lower_priority_dma_at_unit_boundary() { + let mut machine = machine(); + machine.memory_mut().write16(SOURCE, 0x1111).unwrap(); + machine.memory_mut().write16(SOURCE + 2, 0x3333).unwrap(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x2000); // HBlank. + configure(machine.memory_mut(), 3, SOURCE + 2, DEST, 3, 0x140); // Immediate fixed fill. + machine.memory_mut().advance_cycles(HBLANK_START - 1); + units(&mut machine, 3, 1); // Crosses HBlank with DMA0 pending. + units(&mut machine, 0, 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111); + units(&mut machine, 3, 2); // Resumes the lower-priority block. + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x3333); + assert!(!enabled(&machine, 3)); +} + +#[test] +fn blank_requests_do_not_depend_on_dispstat_or_forced_blank() { + for (timing, edge) in [(0x1000, VBLANK_START), (0x2000, HBLANK_START)] { + let mut machine = machine(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing); + machine.memory_mut().write16(DISPCNT, 0x80).unwrap(); + machine.memory_mut().advance_cycles(edge - 1); + assert_eq!(machine.memory().read16(DISPSTAT).unwrap() & 0x38, 0); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Clock crosses edge. + units(&mut machine, 0, 1); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + } +} + +#[test] +fn enabling_during_blank_waits_for_next_edge_and_zero_advance_does_not_trigger() { + for (timing, edge, period) in [ + (0x1000, VBLANK_START, CYCLES_PER_FRAME), + (0x2000, HBLANK_START, CYCLES_PER_LINE), + ] { + let mut machine = machine(); + machine.memory_mut().advance_cycles(edge); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, timing); + machine.memory_mut().advance_cycles(0); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + let elapsed = (machine.cycles() - u64::from(edge)) as u32; + machine.memory_mut().advance_cycles(period - elapsed); + units(&mut machine, 0, 1); + } +} + +#[test] +fn hblank_dma_runs_on_all_visible_lines_but_not_hidden_lines() { + let mut machine = machine(); + configure(machine.memory_mut(), 1, SOURCE, DEST, 1, 0x2340); // Repeat HBlank, fixed addresses. + machine.memory_mut().write16(DISPSTAT, 0x10).unwrap(); + for line in 0..228_u64 { + let edge = line * u64::from(CYCLES_PER_LINE) + u64::from(HBLANK_START); + advance_to(&mut machine, edge); + assert_eq!(machine.memory().read16(IF).unwrap(), 2); // HBlank IRQ still occurs on hidden lines. + machine.memory_mut().write16(IF, 2).unwrap(); + if line < 160 { + units(&mut machine, 1, 1); + } else { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + } + let edge = u64::from(CYCLES_PER_FRAME + HBLANK_START); + advance_to(&mut machine, edge); + units(&mut machine, 1, 1); +} + +#[test] +fn vblank_repeat_preserves_source_reloads_destination_and_count() { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap(); + machine + .memory_mut() + .write32(SOURCE + 4, 0x4444_3333) + .unwrap(); + configure(machine.memory_mut(), 3, SOURCE, DEST, 2, 0x5260); // VBlank, repeat, dest reload, IRQ. + machine.memory_mut().advance_cycles(VBLANK_START); + units(&mut machine, 3, 2); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111); + assert!(enabled(&machine, 3)); + assert_eq!(machine.memory().read16(IF).unwrap(), 0x800); + machine.memory_mut().write16(IF, 0x800).unwrap(); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); // Same VBlank does not retrigger. + let next = u64::from(VBLANK_START + CYCLES_PER_FRAME); + advance_to(&mut machine, next); + units(&mut machine, 3, 2); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x4444_3333); + assert_eq!(machine.memory().read16(IF).unwrap(), 0x800); + assert!(enabled(&machine, 3)); +} + +#[test] +fn repeated_increment_destination_continues_and_reload_uses_programmed_values() { + for reload in [false, true] { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap(); + machine + .memory_mut() + .write32(SOURCE + 4, 0x4444_3333) + .unwrap(); + configure( + machine.memory_mut(), + 0, + SOURCE, + DEST, + 1, + 0x1200 | if reload { 0x60 } else { 0 }, + ); + // Initial count/address are latched. Repeat reloads use newly programmed values. + machine.memory_mut().write16(base(0) + 8, 2).unwrap(); + machine + .memory_mut() + .write32(base(0) + 4, DEST + 16) + .unwrap(); + machine.memory_mut().advance_cycles(VBLANK_START); + units(&mut machine, 0, 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1111); + let next = u64::from(VBLANK_START + CYCLES_PER_FRAME); + advance_to(&mut machine, next); + units(&mut machine, 0, 2); + let address = if reload { DEST + 16 } else { DEST + 2 }; + assert_eq!(machine.memory().read16(address).unwrap(), 0x2222); + assert_eq!(machine.memory().read16(address + 2).unwrap(), 0x3333); + } +} + +#[test] +fn pending_requests_coalesce_and_busy_channel_does_not_queue_repeats() { + let mut machine = machine(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x1200); + machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 3); + units(&mut machine, 0, 1); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + + let mut machine = self::machine(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 100, 0x2200); // Takes 402 cycles, beyond HBlank. + machine.memory_mut().advance_cycles(HBLANK_START); + units(&mut machine, 0, 99); + // Inject more display edges while the block is still active; no queued repeat. + machine.memory_mut().advance_cycles(CYCLES_PER_FRAME * 2); + units(&mut machine, 0, 1); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); +} + +#[test] +fn timer_and_display_clocks_continue_during_dma_and_latch_all_irqs() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_ffff) + .unwrap(); + machine.memory_mut().write16(DISPSTAT, 0x10).unwrap(); + machine.memory_mut().advance_cycles(HBLANK_START - 2); + machine.memory_mut().write16(IF, 0x3fff).unwrap(); + configure(machine.memory_mut(), 2, SOURCE, DEST, 1, 0x4000); + let before = machine.cycles(); + units(&mut machine, 2, 1); + assert_eq!(machine.cycles(), before + 6); + assert_eq!( + machine.memory().display_position().line_cycle, + HBLANK_START as u16 + 4 + ); + assert_eq!(machine.memory().read16(IF).unwrap(), 0x40a); + assert!(!machine.memory().irq_pending()); // IE/IME are off, requests still latch. + machine.memory_mut().write16(IE, 0x400).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + assert!(machine.memory().irq_pending()); +} + +#[test] +fn dma_completion_irq_enters_handler_acknowledges_and_returns_without_skipping_cpu() { + for channel in 0..4 { + let mask = 1_u16 << (8 + channel); + let mut bios = vec![0; BIOS_SIZE]; + bios[0x18..0x24].copy_from_slice(&words(&[ + 0xe1c1_20b2, // STRH r2,[r1,#2] (IF) + 0xe280_0001, // ADD r0,r0,#1 + 0xe25e_f004, // SUBS pc,lr,#4 + ])); + let rom = words(&[ + 0xe59f_1008, // LDR r1,[pc,#8] (IE) + 0xe59f_2008, // LDR r2,[pc,#8] (mask) + 0xe283_3001, // ADD r3,r3,#1 (interrupted instruction) + 0xeaff_fffe, + IE, + u32::from(mask), + ]); + let mut bus = Memory::with_bios(rom, bios).unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut bus).unwrap(); + cpu.step(&mut bus).unwrap(); + configure(&mut bus, channel, SOURCE, DEST, 2, 0x4000); + bus.write16(IE, mask).unwrap(); + bus.write16(IME, 1).unwrap(); + let mut machine = Machine::new(cpu, bus); + units(&mut machine, channel, 1); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); // Not yet complete. + units(&mut machine, channel, 1); + assert_eq!(machine.memory().read16(IF).unwrap(), mask); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().mode(), Mode::Irq); + assert_eq!(machine.cpu().registers()[14], ROM_START + 12); + for _ in 0..3 { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.cpu().mode(), Mode::System); + assert_eq!(machine.cpu().pc(), ROM_START + 8); + assert_eq!(machine.cpu().registers()[0], 1); + assert_eq!(machine.cpu().registers()[3], 0); + machine.step().unwrap(); + assert_eq!(machine.cpu().registers()[3], 1); + } +} + +#[test] +fn rom_dma_uses_waitcnt_sequential_costs_and_forces_source_increment() { + for channel in 1..4 { + for source_mode in 0..3 { + for (window, waitcnt) in [(0x0800_0000, 0), (0x0a00_0000, 0x7fff), (0x0c00_0000, 0)] { + for word in [false, true] { + let mut bus = Memory::new(words(&[0x2222_1111, 0x4444_3333])).unwrap(); + bus.write16(WAITCNT, waitcnt).unwrap(); + let width = if word { + AccessWidth::Word + } else { + AccessWidth::Halfword + }; + let stride = if word { 4 } else { 2 }; + configure( + &mut bus, + channel, + window, + DEST, + 2, + source_mode << 7 | if word { 0x400 } else { 0 }, + ); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + for index in 0..2 { + units(&mut machine, channel, 1); + let expected = bus_cycles( + machine.memory().waitcnt(), + window + index * stride, + width, + if index == 0 { + AccessKind::NonSequential + } else { + AccessKind::Sequential + }, + ) + 1; + assert_eq!(machine.last_timing().data_cycles, expected); + assert_eq!( + machine.last_timing().internal_cycles, + if index == 0 { 2 } else { 0 } + ); + } + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2222_1111); + if word { + assert_eq!(machine.memory().read32(DEST + 4).unwrap(), 0x4444_3333); + } + } + } + } + } +} + +#[test] +fn rom_boundary_forces_nonsequential_timing_in_middle_of_dma() { + let mut machine = Machine::new( + Cpu::new(ROM_START), + Memory::new(vec![0x5a; 0x20004]).unwrap(), + ); + configure(machine.memory_mut(), 3, ROM_START + 0x1fffe, DEST, 3, 0); + for expected in [8, 6, 4] { + // First: 5+1+2; boundary: 5+1; then 3+1. + units(&mut machine, 3, 1); + assert_eq!(machine.last_timing().total(), expected); + } + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x5a5a_5a5a); +} + +#[test] +fn overlapping_copies_read_each_unit_after_previous_write() { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x2222_1111).unwrap(); + configure(machine.memory_mut(), 0, SOURCE, SOURCE + 2, 3, 0); + units(&mut machine, 0, 3); + for offset in [0, 2, 4, 6] { + assert_eq!(machine.memory().read16(SOURCE + offset).unwrap(), 0x1111); + } +} + +#[test] +fn video_palette_mirrors_and_mapped_io_use_the_normal_bus() { + for (destination, alias) in [ + (VRAM_START + 0x18000, VRAM_START + 0x10000), + (PALETTE_START + 0x400, PALETTE_START), + (DISPCNT, DISPCNT), + ] { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0x0001_0403).unwrap(); + configure(machine.memory_mut(), 3, SOURCE, destination, 1, 0x400); + units(&mut machine, 3, 1); + assert_eq!(machine.memory().read32(alias).unwrap(), 0x0001_0403); + } +} + +#[test] +fn unsupported_controls_are_explicit_and_leave_state_and_time_unchanged() { + for channel in 0..4 { + for control in [0x180, 0x3000, 0x3800] { + let mut machine = machine(); + configure(machine.memory_mut(), channel, SOURCE, DEST, 1, control); + let before = machine.cpu().clone(); + let stored = machine.memory().read16(base(channel) + 10).unwrap(); + for _ in 0..2 { + assert_eq!( + machine.step(), + Err(MachineError::Dma(DmaError::UnsupportedControl { + channel, + control: stored + })) + ); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.last_timing().total(), 0); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0); + assert!(enabled(&machine, channel)); + } + } + } + let mut machine = machine(); + configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x800); // DRQ even without special timing. + assert!(matches!( + machine.step(), + Err(MachineError::Dma(DmaError::UnsupportedControl { .. })) + )); +} + +#[test] +fn invalid_memory_and_dma_register_destinations_are_diagnostics() { + for (source, destination, error) in [ + ( + 0, + DEST, + DmaError::UnsupportedSource { + channel: 3, + address: 0, + }, + ), + ( + 0x0e00_0000, + DEST, + DmaError::Memory { + channel: 3, + error: MemoryError::Unmapped(0x0e00_0000), + }, + ), + ( + SOURCE, + ROM_START, + DmaError::Memory { + channel: 3, + error: MemoryError::ReadOnly(ROM_START), + }, + ), + ( + SOURCE, + 0x0e00_0000, + DmaError::Memory { + channel: 3, + error: MemoryError::Unmapped(0x0e00_0000), + }, + ), + ( + SOURCE, + DMA_BASE, + DmaError::RegisterDestination { + channel: 3, + address: DMA_BASE, + }, + ), + ] { + let mut machine = machine(); + configure(machine.memory_mut(), 3, source, destination, 1, 0x4000); + let before = machine.cpu().clone(); + assert_eq!(machine.step(), Err(MachineError::Dma(error))); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert!(enabled(&machine, 3)); + } +} + +#[test] +fn failing_later_unit_preserves_earlier_data_but_not_partial_current_unit() { + let mut machine = Machine::new( + Cpu::new(ROM_START), + Memory::new(vec![0x11, 0x22, 0x33]).unwrap(), + ); + configure(machine.memory_mut(), 3, ROM_START, DEST, 2, 0x4000); + units(&mut machine, 3, 1); + let timing = machine.last_timing(); + let cycles = machine.cycles(); + let position = machine.memory().display_position(); + for _ in 0..2 { + assert_eq!( + machine.step(), + Err(MachineError::Dma(DmaError::Memory { + channel: 3, + error: MemoryError::Unmapped(ROM_START + 3) + })) + ); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x2211); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.memory().display_position(), position); + assert_eq!(machine.last_timing(), timing); + assert!(enabled(&machine, 3)); + } +} + +#[test] +fn failed_io_write_does_not_partially_change_display_control() { + let mut machine = machine(); + machine.memory_mut().write32(SOURCE, 0xffff_ffff).unwrap(); + configure(machine.memory_mut(), 3, SOURCE, 0x0400_0058, 1, 0x400); + assert!(machine.step().is_err()); + assert_eq!(machine.memory().read16(DISPCNT).unwrap(), 0); + assert_eq!(machine.cycles(), 0); +} + +#[test] +fn cpu_can_enable_dma_with_a_combined_count_control_store() { + let rom = words(&[ + 0xe59f_0010, // LDR r0,[pc,#16] (DMA3CNT) + 0xe59f_1010, // LDR r1,[pc,#16] (count/control) + 0xe580_1000, // STR r1,[r0] + 0xe282_2001, // ADD r2,r2,#1 (must wait for transfer) + 0xeaff_fffe, + 0xe1a0_0000, + base(3) + 8, + 0x8400_0002, + ]); + let mut bus = Memory::new(rom).unwrap(); + bus.write32(SOURCE, 0x1234_5678).unwrap(); + bus.write32(base(3), SOURCE).unwrap(); + bus.write32(base(3) + 4, DEST).unwrap(); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + for _ in 0..3 { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + assert_eq!(machine.cpu().pc(), ROM_START + 12); + units(&mut machine, 3, 2); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0x1234_5678); + assert_eq!(machine.cpu().registers()[2], 0); + machine.step().unwrap(); + assert_eq!(machine.cpu().registers()[2], 1); +} + +#[test] +fn failed_cpu_block_store_cannot_enable_dma_or_change_its_hidden_latches() { + let rom = words(&[ + 0xe59f_000c, // LDR r0,[pc,#12] (DMA3CNT) + 0xe59f_100c, // LDR r1,[pc,#12] (count/control) + 0xe8a0_0006, // STMIA r0!,{r1,r2}; second word unmapped. + 0xeaff_fffe, + 0xe1a0_0000, + base(3) + 8, + 0x8400_0001, + ]); + let mut bus = Memory::new(rom).unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut bus).unwrap(); + cpu.step(&mut bus).unwrap(); + configure(&mut bus, 3, SOURCE, DEST, 2, 0); + bus.write16(base(3) + 10, 0).unwrap(); + let before = cpu.clone(); + let mut machine = Machine::new(cpu, bus); + assert!(matches!(machine.step(), Err(MachineError::Cpu(_)))); + assert_eq!(machine.cpu(), &before); + assert!(!enabled(&machine, 3)); + assert_eq!(machine.cycles(), 0); + machine.memory_mut().write16(base(3) + 10, 0x8000).unwrap(); + units(&mut machine, 3, 2); // The count latch is still 2, not 1. + assert!(!enabled(&machine, 3)); +} + +#[test] +fn cpu_only_and_clock_only_calls_never_execute_dma() { + let mut bus = Memory::new(words(&[0xe280_0001])).unwrap(); + bus.write16(SOURCE, 0x1234).unwrap(); + configure(&mut bus, 0, SOURCE, DEST, 1, 0); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut bus).unwrap(); + bus.advance_cycles(CYCLES_PER_FRAME); + assert_eq!(cpu.registers()[0], 1); + assert_eq!(bus.read16(DEST).unwrap(), 0); + let mut machine = Machine::new(cpu, bus); + units(&mut machine, 0, 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234); +} + +#[test] +fn frame_runner_counts_dma_units_obeys_limits_and_reports_dma_errors() { + let mut machine = machine(); + configure(machine.memory_mut(), 0, SOURCE, DEST, 3, 0); + assert_eq!( + machine.run_until_vblank(2), + Err(FrameRunError::StepLimit(2)) + ); + assert_eq!(machine.cpu().pc(), ROM_START); + assert_eq!(machine.cycles(), 10); + machine.memory_mut().advance_cycles(VBLANK_START - 11); + assert_eq!(machine.run_until_vblank(1).unwrap(), 1); + assert_eq!(machine.memory().display_position().scanline, 160); + assert_eq!(machine.cpu().pc(), ROM_START); + configure(machine.memory_mut(), 0, SOURCE, DEST, 1, 0x3000); + assert!(matches!( + machine.run_until_vblank(1), + Err(FrameRunError::Dma(_)) + )); +} + +#[test] +fn vblank_runner_stops_at_event_before_newly_requested_dma_executes() { + let mut machine = machine(); + machine.memory_mut().write16(SOURCE, 0x1234).unwrap(); + configure(machine.memory_mut(), 3, SOURCE, DEST, 1, 0x1000); + machine.memory_mut().advance_cycles(VBLANK_START - 1); + assert_eq!(machine.run_until_vblank(1).unwrap(), 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0); + units(&mut machine, 3, 1); + assert_eq!(machine.memory().read16(DEST).unwrap(), 0x1234); +} diff --git a/tests/halt.rs b/tests/halt.rs new file mode 100644 index 0000000..4863ab9 --- /dev/null +++ b/tests/halt.rs @@ -0,0 +1,687 @@ +use gba_rust::{ + cpu::{Cpu, InstructionSet, Mode}, + display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, HBLANK_START, VBLANK_START}, + dma::{DmaError, DMA_BASE}, + io::{DISPSTAT, HALTCNT, IE, IF, IME, POSTFLG, TIMER_BASE}, + machine::{FrameRunError, Machine, MachineError, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, + timing::StepTiming, +}; + +fn words(code: &[u32]) -> Vec { + code.iter().flat_map(|word| word.to_le_bytes()).collect() +} + +fn machine() -> Machine { + Machine::new( + Cpu::new(ROM_START), + Memory::new(words(&[0xe280_0001, 0xeaff_fffe])).unwrap(), + ) +} + +fn pending_timer(bus: &mut Memory) { + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.advance_cycles(1); + bus.write16(TIMER_BASE + 2, 0).unwrap(); +} + +fn halt(machine: &mut Machine) { + // Bare bus writes are explicit host/debug setup, not CPU-originated accesses. + machine.memory_mut().write8(HALTCNT, 0).unwrap(); +} + +fn dma(bus: &mut Memory, control: u16, count: u16) { + bus.write32(DMA_BASE, 0x0200_0000).unwrap(); + bus.write32(DMA_BASE + 4, 0x0300_0000).unwrap(); + bus.write32( + DMA_BASE + 8, + u32::from(control | 0x8000) << 16 | u32::from(count), + ) + .unwrap(); +} + +// Prepare a real store at entry+8 with operands loaded by CPU instructions. +fn store_machine( + entry: u32, + instruction: u32, + address: u32, + value: u32, + timed_setup: bool, +) -> Machine { + let program = words(&[ + 0xe59f_1008, + 0xe59f_0008, + instruction, + 0xe282_2001, + address, + value, + ]); + let mut bios = vec![0; BIOS_SIZE]; + let mut bus = if entry < BIOS_SIZE as u32 { + bios[entry as usize..entry as usize + program.len()].copy_from_slice(&program); + Memory::with_bios(vec![], bios).unwrap() + } else if entry == ROM_START { + Memory::with_bios(program, bios).unwrap() + } else { + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + for (offset, value) in program.into_iter().enumerate() { + bus.write8(entry + offset as u32, value).unwrap(); + } + bus + }; + let mut cpu = Cpu::new(entry); + for _ in 0..2 { + if timed_setup { + cpu.step_timed(&mut bus).unwrap(); + } else { + cpu.step(&mut bus).unwrap(); + } + } + Machine::new(cpu, bus) +} + +#[test] +fn reset_register_masks_widths_and_reserved_bytes() { + let mut bus = Memory::new(vec![]).unwrap(); + assert!(!bus.halted()); + assert_eq!(bus.read32(POSTFLG).unwrap(), 0); + bus.write8(POSTFLG, 0xff).unwrap(); + assert_eq!(bus.read32(POSTFLG).unwrap(), 1); + assert!(!bus.halted()); + bus.write8(POSTFLG, 0xfe).unwrap(); + assert_eq!(bus.read8(POSTFLG).unwrap(), 0); + bus.write16(POSTFLG + 2, 0xffff).unwrap(); + assert_eq!(bus.read16(POSTFLG + 2).unwrap(), 0); + assert!(!bus.halted()); + bus.write16(POSTFLG, 0x7f01).unwrap(); + assert!(bus.halted()); + assert_eq!(bus.read16(POSTFLG).unwrap(), 1); // HALTCNT is write-only. + assert_eq!(bus.read16(HALTCNT), Err(MemoryError::Unaligned(HALTCNT))); + assert_eq!( + bus.read8(POSTFLG + 4), + Err(MemoryError::Unmapped(POSTFLG + 4)) + ); + assert_eq!( + bus.read8(POSTFLG + 0x400), + Err(MemoryError::Unmapped(POSTFLG + 0x400)) + ); +} + +#[test] +fn all_halt_values_ignore_low_seven_bits() { + for value in 0..=0x7f { + let mut machine = machine(); + machine.memory_mut().write8(HALTCNT, value).unwrap(); + assert!(machine.halted()); + assert_eq!(machine.memory().read8(HALTCNT).unwrap(), 0); + assert_eq!(machine.cycles(), 0); + } +} + +#[test] +fn halt_idle_does_not_fetch_instructions_or_change_cpu_registers() { + let mut machine = Machine::new(Cpu::new(0xdead_beec), Memory::new(vec![]).unwrap()); + halt(&mut machine); + let before = machine.cpu().clone(); + for cycles in [HBLANK_START, CYCLES_PER_LINE - HBLANK_START, HBLANK_START] { + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.cpu(), &before); + assert_eq!( + machine.last_timing(), + StepTiming { + idle_cycles: cycles, + ..StepTiming::default() + } + ); + assert!(machine.halted()); + } + assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE + HBLANK_START)); +} + +#[test] +fn pending_enabled_request_prevents_sleep_regardless_of_ime() { + for ime in [0, 1] { + let mut machine = machine(); + pending_timer(machine.memory_mut()); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, ime).unwrap(); + halt(&mut machine); + assert!(!machine.halted()); + assert_eq!( + machine.step().unwrap(), + if ime == 0 { + StepKind::Instruction + } else { + StepKind::IrqEntry + } + ); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + } +} + +#[test] +fn ie_gates_wake_but_ime_does_not_and_wake_is_not_reversed_by_acknowledgement() { + let mut machine = machine(); + pending_timer(machine.memory_mut()); + machine.memory_mut().write16(IE, 1).unwrap(); // Wrong source. + halt(&mut machine); + assert!(machine.halted()); + machine.memory_mut().write16(IME, 1).unwrap(); + assert!(machine.halted()); + machine.memory_mut().write16(IE, 8).unwrap(); + assert!(!machine.halted()); + machine.memory_mut().write16(IF, 8).unwrap(); + machine.memory_mut().write16(IE, 0).unwrap(); + assert!(!machine.halted()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); +} + +#[test] +fn timer_wakes_at_exact_overflow_without_ime_and_resumes_next_instruction() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_fff6) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + halt(&mut machine); + let before = machine.cpu().clone(); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, 10); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 10); + assert!(!machine.halted()); + assert!(!machine.memory().irq_pending()); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[0], 1); + assert_eq!(machine.cpu().pc(), ROM_START + 4); +} + +#[test] +fn cpsr_irq_mask_blocks_handler_but_not_halt_wake() { + let mut bios = vec![0; BIOS_SIZE]; + bios[0..4].copy_from_slice(&0xe280_0001_u32.to_le_bytes()); // ADD r0,r0,#1 + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.write16(IE, 8).unwrap(); + bus.write16(IME, 1).unwrap(); + bus.write8(HALTCNT, 0).unwrap(); + let mut machine = Machine::new(Cpu::at_reset(), bus); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert!(!machine.halted()); + assert!(machine.memory().irq_pending()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().mode(), Mode::Supervisor); + assert_eq!(machine.cpu().registers()[0], 1); +} + +#[test] +fn unmasked_wake_enters_irq_before_executing_resumed_instruction() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_ffff) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().mode(), Mode::Irq); + assert_eq!(machine.cpu().pc(), 0x18); + assert_eq!(machine.cpu().registers()[14], ROM_START + 4); + assert_eq!(machine.cpu().registers()[0], 0); +} + +#[test] +fn wake_irq_returns_to_thumb_without_skipping_instruction() { + let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0,pc,#1; BX r0 + rom.extend(0x2107_u16.to_le_bytes()); // MOV r1,#7 + let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc,lr,#4 + let mut bus = Memory::with_bios(rom, bios).unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut bus).unwrap(); + cpu.step(&mut bus).unwrap(); + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.write16(IE, 8).unwrap(); + bus.write16(IME, 1).unwrap(); + bus.write8(HALTCNT, 0).unwrap(); + let mut machine = Machine::new(cpu, bus); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().spsr(), Some(0x3f)); + machine.memory_mut().write16(TIMER_BASE + 2, 0).unwrap(); + machine.memory_mut().write16(IF, 8).unwrap(); + machine.step().unwrap(); + assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb); + assert_eq!(machine.cpu().pc(), ROM_START + 8); + machine.step().unwrap(); + assert_eq!(machine.cpu().registers()[1], 7); +} + +#[test] +fn all_display_interrupt_sources_can_wake_halt_at_their_edges() { + for (control, mask, edge) in [ + (8, 1, VBLANK_START), + (0x10, 2, HBLANK_START), + (0x0120, 4, CYCLES_PER_LINE), + ] { + let mut machine = machine(); + machine.memory_mut().write16(DISPSTAT, control).unwrap(); + machine.memory_mut().write16(IE, mask).unwrap(); + machine.memory_mut().advance_cycles(edge - 1); + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, 1); + assert_eq!(machine.cycles(), u64::from(edge)); + assert!(!machine.halted()); + assert_eq!(machine.memory().read16(IF).unwrap(), mask); + } +} + +#[test] +fn local_interrupt_enable_is_required_to_generate_a_wake_request() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x0080_ffff) + .unwrap(); // IRQ off. + machine.memory_mut().write16(IE, 0x3fff).unwrap(); + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, 1); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert!(machine.halted()); +} + +#[test] +fn idle_scheduling_accounts_for_timer_prescaler_phase() { + for (selection, divisor) in [1, 64, 256, 1024].into_iter().enumerate() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, ((0xc0 | selection as u32) << 16) | 0xffff) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().advance_cycles(divisor - 1); + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, 1); + assert_eq!(machine.cycles(), u64::from(divisor)); + assert!(!machine.halted()); + } +} + +#[test] +fn cascaded_timer_wakes_only_on_predecessor_overflow() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x0080_fff6) + .unwrap(); // Ten cycles per overflow, IRQ off. + machine + .memory_mut() + .write32(TIMER_BASE + 4, 0x00c4_fffe) + .unwrap(); // Two pulses, IRQ on. + machine.memory_mut().write16(IE, 0x10).unwrap(); + halt(&mut machine); + for expected in [true, false] { + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, 10); + assert_eq!(machine.halted(), expected); + } + assert_eq!(machine.memory().read16(IF).unwrap(), 0x10); + assert_eq!(machine.cycles(), 20); +} + +#[test] +fn disabled_predecessor_cannot_clock_a_cascaded_timer_during_halt() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE + 4, 0x00c4_ffff) + .unwrap(); + machine.memory_mut().write16(IE, 0x10).unwrap(); + halt(&mut machine); + for _ in 0..4 { + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert!(machine.halted()); + assert_eq!(machine.memory().read16(TIMER_BASE + 4).unwrap(), 0xffff); + } +} + +#[test] +fn idle_batches_match_cycle_by_cycle_display_and_timer_execution() { + for seed in 0..16_u32 { + let configure = || { + let mut bus = Memory::new(vec![]).unwrap(); + bus.write16(DISPSTAT, ((seed as u16 * 17) << 8) | 0x38) + .unwrap(); + for index in 0..4 { + let control = if index != 0 && (seed + index) % 3 == 0 { + 0xc4 + } else { + 0xc0 | ((seed + index) & 3) + }; + bus.write32( + TIMER_BASE + index * 4, + control << 16 | (0xffff - (seed * 13 + index * 7)), + ) + .unwrap(); + } + bus.advance_cycles(seed * 91); + bus.write16(IF, 0x3fff).unwrap(); + bus.write8(HALTCNT, 0).unwrap(); // IE=0, so all events are observable without wake. + bus + }; + let mut reference = configure(); + let mut machine = Machine::new(Cpu::new(ROM_START), configure()); + for _ in 0..40 { + machine.memory_mut().write16(IF, 0x3fff).unwrap(); + reference.write16(IF, 0x3fff).unwrap(); + let mut elapsed = 0; + loop { + reference.advance_cycles(1); + elapsed += 1; + let position = reference.display_position(); + if position.line_cycle == 0 + || u32::from(position.line_cycle) == HBLANK_START + || reference.read16(IF).unwrap() != 0 + { + break; + } + assert!(elapsed < CYCLES_PER_LINE); + } + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.last_timing().idle_cycles, elapsed); + assert_eq!( + machine.memory().display_position(), + reference.display_position() + ); + assert_eq!(machine.cycles(), reference.cycles()); + assert_eq!( + machine.memory().read16(IF).unwrap(), + reference.read16(IF).unwrap() + ); + for index in 0..4 { + assert_eq!( + machine.memory().read32(TIMER_BASE + index * 4).unwrap(), + reference.read32(TIMER_BASE + index * 4).unwrap() + ); + } + } + } +} + +#[test] +fn blank_triggered_dma_continues_during_halt_and_completion_can_wake() { + let mut machine = machine(); + machine.memory_mut().write16(0x0200_0000, 0x1234).unwrap(); + dma(machine.memory_mut(), 0x5000, 1); // VBlank and completion IRQ. + machine.memory_mut().write16(IE, 0x100).unwrap(); + halt(&mut machine); + assert_eq!(machine.run_until_vblank(320).unwrap(), 320); + assert!(machine.halted()); // No display IRQ enabled; DMA has not run yet. + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 }); + assert_eq!(machine.memory().read16(0x0300_0000).unwrap(), 0x1234); + assert_eq!(machine.memory().read16(IF).unwrap(), 0x100); + assert!(!machine.halted()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); +} + +#[test] +fn dma_without_completion_irq_does_not_wake_halt() { + let mut machine = machine(); + dma(machine.memory_mut(), 0x2000, 1); // HBlank, no IRQ. + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.cycles(), u64::from(HBLANK_START)); + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 }); + assert!(machine.halted()); + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.cycles(), u64::from(CYCLES_PER_LINE)); +} + +#[test] +fn dma_finishes_before_irq_delivery_after_wake_during_transfer() { + let mut machine = machine(); + dma(machine.memory_mut(), 0, 2); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_ffff) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + halt(&mut machine); + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 }); + assert!(!machine.halted()); + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 }); + assert_eq!(machine.cpu().pc(), ROM_START); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); +} + +#[test] +fn unsupported_dma_power_control_write_preserves_halt_and_clock() { + let mut machine = machine(); + dma(machine.memory_mut(), 0, 1); + machine.memory_mut().write16(DMA_BASE + 10, 0).unwrap(); + machine.memory_mut().write32(DMA_BASE + 4, POSTFLG).unwrap(); + machine.memory_mut().write16(DMA_BASE + 10, 0x8000).unwrap(); + halt(&mut machine); + assert_eq!( + machine.step(), + Err(MachineError::Dma(DmaError::PowerControlDestination { + channel: 0, + address: POSTFLG + })) + ); + assert!(machine.halted()); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0); +} + +#[test] +fn byte_halfword_and_word_cpu_stores_enter_halt_only_from_bios() { + for timed in [false, true] { + for entry in [0x100, ROM_START, 0x0300_0000] { + for (instruction, address, value) in [ + (0xe5c1_0000, HALTCNT, 0x7f), // STRB + (0xe1c1_00b0, POSTFLG, 1), // STRH + (0xe581_0000, POSTFLG, 0xffff_0001), // STR + ] { + let mut machine = store_machine(entry, instruction, address, value, timed); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.halted(), entry < BIOS_SIZE as u32); + assert_eq!(machine.cpu().pc(), entry + 12); + assert_eq!(machine.cpu().registers()[2], 0); + let before = machine.cpu().clone(); + if entry < BIOS_SIZE as u32 { + assert_eq!(machine.step().unwrap(), StepKind::HaltIdle); + assert_eq!(machine.cpu(), &before); + } else { + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[2], 1); + } + } + } + } +} + +#[test] +fn thumb_halt_stores_use_the_executing_address_for_bios_access() { + for entry in [0x100, ROM_START] { + let mut code = words(&[0xe28f_0001, 0xe12f_ff10]); // Enter Thumb at entry+8. + for instruction in [0x2000_u16, 0x4901, 0x7008, 0x3201] { + // MOV r0,#0; LDR r1,[pc,#4]; STRB r0,[r1]; ADD r2,#1. + code.extend(instruction.to_le_bytes()); + } + code.extend(HALTCNT.to_le_bytes()); + let mut bios = vec![0; BIOS_SIZE]; + let mut bus = if entry < BIOS_SIZE as u32 { + bios[entry as usize..entry as usize + code.len()].copy_from_slice(&code); + Memory::with_bios(vec![], bios).unwrap() + } else { + Memory::new(code).unwrap() + }; + let mut cpu = Cpu::new(entry); + for _ in 0..4 { + cpu.step(&mut bus).unwrap(); + } + let mut machine = Machine::new(cpu, bus); + assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().pc(), entry + 14); + assert_eq!(machine.halted(), entry < BIOS_SIZE as u32); + } +} + +#[test] +fn halt_store_finishes_and_can_wake_during_its_own_cycles() { + let mut machine = store_machine(0x100, 0xe5c1_0000, HALTCNT, 0, false); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_ffff) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().pc(), 0x10c); + assert_eq!(machine.cycles(), 2); + assert!(!machine.halted()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[2], 1); +} + +#[test] +fn stop_diagnostics_validate_entire_write_before_postflg_or_halt_changes() { + let mut bus = Memory::new(vec![]).unwrap(); + for value in [0x80, 0xff] { + assert_eq!( + bus.write8(HALTCNT, value), + Err(MemoryError::UnsupportedStop) + ); + assert_eq!( + bus.write16(POSTFLG, u16::from(value) << 8 | 1), + Err(MemoryError::UnsupportedStop) + ); + assert_eq!( + bus.write32(POSTFLG, u32::from(value) << 8 | 1), + Err(MemoryError::UnsupportedStop) + ); + assert_eq!(bus.read32(POSTFLG).unwrap(), 0); + assert!(!bus.halted()); + assert_eq!(bus.cycles(), 0); + } +} + +#[test] +fn bios_stop_error_preserves_cpu_timing_and_clears_access_context() { + let mut machine = store_machine(0x100, 0xe581_0000, POSTFLG, 0x8001, true); + let before = machine.cpu().clone(); + assert_eq!( + machine.step(), + Err(MachineError::Cpu(MemoryError::UnsupportedStop.into())) + ); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.last_timing(), StepTiming::default()); + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0); + assert!(!machine.halted()); + // A failed non-BIOS instruction must also clear its access context. + let mut machine = store_machine(ROM_START, 0xe581_0000, 0x0400_0058, 0, true); + assert!(machine.step().is_err()); + machine.memory_mut().write8(POSTFLG, 1).unwrap(); + halt(&mut machine); + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 1); + assert!(machine.halted()); +} + +#[test] +fn non_bios_stop_write_is_ignored_not_a_diagnostic() { + for entry in [ROM_START, 0x0300_0000] { + let mut machine = store_machine(entry, 0xe581_0000, POSTFLG, 0x8001, false); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert!(!machine.halted()); + assert_eq!(machine.memory().read32(POSTFLG).unwrap(), 0); + } +} + +#[test] +fn failed_block_store_cannot_partially_halt_or_change_postflg() { + // The first word is mapped; the second is not. Validation precedes all writes. + let mut machine = store_machine(0x100, 0xe8a1_0005, POSTFLG, 1, false); // STMIA r1!,{r0,r2} + let before = machine.cpu().clone(); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert!(!machine.halted()); + assert_eq!(machine.memory().read8(POSTFLG).unwrap(), 0); + assert_eq!(machine.cycles(), 0); +} + +#[test] +fn cpu_only_apis_record_halt_but_do_not_idle_or_enforce_pause() { + for timed in [false, true] { + let mut bios = vec![0; BIOS_SIZE]; + bios[..24].copy_from_slice(&words(&[ + 0xe59f_1008, + 0xe3a0_0000, + 0xe5c1_0000, + 0xe282_2001, + HALTCNT, + 0, + ])); + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + let mut cpu = Cpu::new(0); + for _ in 0..3 { + if timed { + cpu.step_timed(&mut bus).unwrap(); + } else { + cpu.step(&mut bus).unwrap(); + } + } + assert!(bus.halted()); + cpu.step(&mut bus).unwrap(); // CPU-only API intentionally bypasses machine scheduling. + assert_eq!(cpu.registers()[2], 1); + assert_eq!(bus.cycles(), 0); + assert!(bus.halted()); + } +} + +#[test] +fn frame_runner_remains_bounded_with_no_wake_source_and_reports_idle_cost() { + let mut machine = Machine::new(Cpu::new(ROM_START), Memory::new(vec![]).unwrap()); + halt(&mut machine); + assert_eq!( + machine.run_until_vblank(0), + Err(FrameRunError::StepLimit(0)) + ); + assert_eq!(machine.cycles(), 0); + assert_eq!( + machine.run_until_vblank(319), + Err(FrameRunError::StepLimit(319)) + ); + assert!(machine.halted()); + assert_eq!(machine.run_until_vblank(1).unwrap(), 1); + assert_eq!(machine.cycles(), u64::from(VBLANK_START)); + assert_eq!(machine.memory().display_position().line_cycle, 0); + assert!(machine.halted()); + assert_eq!(machine.run_until_vblank(456).unwrap(), 456); + assert_eq!(machine.cycles(), u64::from(VBLANK_START + CYCLES_PER_FRAME)); +} + +#[test] +fn externally_advanced_clock_can_wake_halt_without_cpu_execution() { + let mut machine = machine(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_fff0) + .unwrap(); + machine.memory_mut().write16(IE, 8).unwrap(); + halt(&mut machine); + let before = machine.cpu().clone(); + machine.memory_mut().advance_cycles(16); + assert!(!machine.halted()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.last_timing(), StepTiming::default()); +} diff --git a/tests/machine.rs b/tests/machine.rs new file mode 100644 index 0000000..f27ba59 --- /dev/null +++ b/tests/machine.rs @@ -0,0 +1,272 @@ +use gba_rust::{ + cpu::{Cpu, CpuError, InstructionSet, Mode}, + io::{IE, IF, IME, TIMER_BASE, WAITCNT}, + machine::{Machine, MachineError, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, + timer_demo::{timer_demo, TIMER_DEMO_STEPS}, +}; + +fn words(values: &[u32]) -> Vec { + values + .iter() + .flat_map(|value| value.to_le_bytes()) + .collect() +} + +fn timer(bus: &mut Memory) { + bus.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + bus.write16(IE, 8).unwrap(); + bus.write32(IME, 1).unwrap(); +} + +fn pending_timer(bus: &mut Memory) { + timer(bus); + bus.advance_cycles(1); + bus.write16(TIMER_BASE + 2, 0).unwrap(); +} + +// Load test operands through real instructions, without exposing CPU setters. +fn prepared(instruction: u32, operands: &[(usize, u32)]) -> Machine { + let offset = (operands.len() as u32 - 1) * 4; + let mut code: Vec = operands + .iter() + .map(|&(register, _)| 0xe59f_0000 | (register as u32) << 12 | offset) + .collect(); + code.push(instruction); + code.extend(operands.iter().map(|&(_, value)| value)); + let mut bus = Memory::new(words(&code)).unwrap(); + let mut cpu = Cpu::new(ROM_START); + for _ in operands { + cpu.step(&mut bus).unwrap(); + } + Machine::new(cpu, bus) +} + +#[test] +fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() { + let mut machine = timer_demo().unwrap(); + let mut entries = 0; + for step in 1..=TIMER_DEMO_STEPS { + let kind = machine.step().unwrap(); + assert!(machine.last_timing().total() > 0); + if kind == StepKind::IrqEntry { + entries += 1; + assert_eq!(step, 13); // Nominal timing model, not a hardware measurement + assert_eq!(machine.cpu().pc(), 0x18); + assert_eq!(machine.cpu().spsr(), Some(0x1f)); + assert_eq!(machine.cpu().registers()[14], ROM_START + 0x30); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + } + } + assert_eq!(entries, 1); + assert_eq!(machine.cycles(), 153); + assert_eq!(machine.cpu().mode(), Mode::System); + assert_eq!(machine.cpu().pc(), ROM_START + 0x2c); + assert_eq!(machine.cpu().registers()[10], 1); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0); + assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4); + for _ in 0..100 { + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + } + assert_eq!(machine.cpu().registers()[10], 1); +} + +#[test] +fn overflow_after_instruction_is_sampled_on_next_step() { + let mut bus = Memory::new(words(&[0xe280_0001, 0xe280_0001])).unwrap(); + timer(&mut bus); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[0], 1); + assert_eq!(machine.cpu().pc(), ROM_START + 4); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute + assert_eq!(machine.cpu().registers()[14], ROM_START + 8); + assert_eq!(machine.cycles(), 11); // First ROM access: 8; BIOS IRQ refill: 3 +} + +#[test] +fn missing_vector_errors_only_after_completed_irq_entry() { + let mut bus = Memory::new(vec![]).unwrap(); + pending_timer(&mut bus); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // No ROM fetch required + let before = machine.cpu().clone(); + let cycles = machine.cycles(); + assert_eq!( + machine.step(), + Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped( + 0x18 + )))) + ); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); +} + +#[test] +fn cpu_mask_defers_irq_until_msr_unmasks_it() { + let mut bios = vec![0; BIOS_SIZE]; + bios[0..4].copy_from_slice(&0xe321_f013_u32.to_le_bytes()); // MSR CPSR_c, #SVC (clear I) + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + pending_timer(&mut bus); + let mut machine = Machine::new(Cpu::at_reset(), bus); + assert!(machine.memory().irq_pending()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().pc(), 4); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().registers()[14], 8); + assert_eq!(machine.cpu().spsr(), Some(0x13)); +} + +#[test] +fn ime_and_ie_gate_pending_device_requests_until_cpu_enables_them() { + for register in [IE, IME] { + let value = if register == IE { 8 } else { 1 }; + let mut machine = prepared(0xe580_1000, &[(0, register), (1, value)]); // STR r1, [r0] + pending_timer(machine.memory_mut()); + machine.memory_mut().write16(register, 0).unwrap(); + assert!(!machine.memory().irq_pending()); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert!(machine.memory().irq_pending()); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + } +} + +#[test] +fn uncleared_request_reenters_after_irq_return_and_no_fiq_is_generated() { + let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); // SUBS pc, lr, #4 + let mut bus = Memory::with_bios(words(&[0xeaff_fffe]), bios).unwrap(); + pending_timer(&mut bus); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + for _ in 0..3 { + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().mode(), Mode::Irq); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); // I mask prevents immediate nesting + assert_eq!(machine.cpu().mode(), Mode::System); + assert_eq!(machine.cpu().pc(), ROM_START); + } + assert_eq!(machine.memory().read16(IF).unwrap(), 8); +} + +#[test] +fn timer_irq_returns_to_thumb_without_skipping_the_interrupted_instruction() { + let mut rom = words(&[0xe28f_0001, 0xe12f_ff10]); // ADD r0, pc, #1; BX r0 + rom.extend(0x2107_u16.to_le_bytes()); // MOV r1, #7 at offset 8 + let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); + let mut bus = Memory::with_bios(rom, bios).unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut bus).unwrap(); + cpu.step(&mut bus).unwrap(); + pending_timer(&mut bus); + let mut machine = Machine::new(cpu, bus); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + assert_eq!(machine.cpu().spsr(), Some(0x3f)); + assert_eq!(machine.cpu().registers()[14], ROM_START + 12); + machine.memory_mut().write16(IF, 8).unwrap(); + machine.step().unwrap(); + assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb); + assert_eq!(machine.cpu().pc(), ROM_START + 8); + machine.step().unwrap(); + assert_eq!(machine.cpu().registers()[1], 7); + assert_eq!(machine.cpu().pc(), ROM_START + 10); +} + +#[test] +fn diagnostics_do_not_advance_timers_or_clock() { + for instruction in [0xf3a0_0001, 0xe590_1000] { + // Unsupported or unmapped LDR + let mut bus = Memory::new(words(&[instruction])).unwrap(); + timer(&mut bus); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + let before = machine.cpu().clone(); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + } +} + +#[test] +fn late_block_store_error_does_not_change_timer_reload_or_enable() { + let mut machine = prepared( + 0xe8a0_003e, + &[ + (0, TIMER_BASE), + (1, 0x00c0_ffff), + (2, 0x00c0_ffff), + (3, 0x00c0_ffff), + (4, 0x00c0_ffff), + (5, 123), + ], + ); // STMIA r0!, {r1-r5}; fifth word is unmapped + let before = machine.cpu().clone(); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + for index in 0..4 { + let address = TIMER_BASE + index * 4; + assert_eq!(machine.memory().read32(address).unwrap(), 0); + machine.memory_mut().write16(address + 2, 0x80).unwrap(); + assert_eq!(machine.memory().read16(address).unwrap(), 0); // Hidden reload also unchanged + } +} + +#[test] +fn late_block_store_error_does_not_acknowledge_if_or_modify_ie() { + let mut machine = prepared( + 0xe8a0_001e, + &[(0, IE), (1, 0x0008_ffff), (2, 123), (3, 1), (4, 42)], + ); + // IE/IF, WAITCNT, IME are valid. The fourth word at 0x0400020c is not. + pending_timer(machine.memory_mut()); + machine.memory_mut().write32(IME, 0).unwrap(); // Let the invalid store execute + let before = machine.cpu().clone(); + let cycles = machine.cycles(); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.memory().read32(IE).unwrap(), 0x0008_0008); + assert_eq!(machine.memory().read32(WAITCNT).unwrap(), 0); + assert_eq!(machine.memory().read32(IME).unwrap(), 0); + assert_eq!(machine.last_timing().total(), 0); +} + +#[test] +fn late_block_load_error_preserves_cpu_and_devices() { + let mut machine = prepared(0xe8b0_0006, &[(0, IME)]); // LDMIA r0!, {r1, r2} + timer(machine.memory_mut()); + let before = machine.cpu().clone(); + assert!(machine.step().is_err()); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.cycles(), 0); + assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xffff); +} + +#[test] +fn skipped_condition_charges_code_access_but_not_registers() { + let mut bus = Memory::new(words(&[0x0280_0001])).unwrap(); // ADDEQ r0, r0, #1; Z clear + timer(&mut bus); + let mut machine = Machine::new(Cpu::new(ROM_START), bus); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[0], 0); + assert_eq!(machine.cpu().pc(), ROM_START + 4); + assert_eq!(machine.cycles(), 8); + assert_eq!(machine.memory().read16(IF).unwrap(), 8); +} + +#[test] +fn cpu_only_step_does_not_advance_device_time_or_deliver_irq() { + let mut bus = Memory::new(words(&[0xe280_0001])).unwrap(); + pending_timer(&mut bus); + let mut cpu = Cpu::new(ROM_START); + let cycles = bus.cycles(); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers()[0], 1); + assert_eq!(cpu.mode(), Mode::System); + assert_eq!(bus.cycles(), cycles); + assert!(bus.irq_pending()); +} diff --git a/tests/timing.rs b/tests/timing.rs new file mode 100644 index 0000000..bf698ce --- /dev/null +++ b/tests/timing.rs @@ -0,0 +1,169 @@ +use gba_rust::{ + cpu::Cpu, + io::{IE, IF, IME, TIMER_BASE, WAITCNT}, + machine::{Machine, StepKind}, + memory::{Memory, MemoryError, ROM_START}, + timing::{bus_cycles, AccessKind, AccessWidth}, +}; + +#[test] +fn waitcnt_masks_unused_bits_and_supports_all_bus_widths() { + let mut memory = Memory::new(vec![]).unwrap(); + assert_eq!(memory.read32(WAITCNT).unwrap(), 0); + memory.write32(WAITCNT, u32::MAX).unwrap(); + assert_eq!(memory.read32(WAITCNT).unwrap(), 0x5fff); + memory.write8(WAITCNT, 0x17).unwrap(); + memory.write8(WAITCNT + 1, 0x43).unwrap(); + assert_eq!(memory.waitcnt(), 0x4317); + memory.write16(WAITCNT + 2, 0xffff).unwrap(); + assert_eq!(memory.read32(WAITCNT).unwrap(), 0x4317); + assert_eq!(memory.read16(IE).unwrap(), 0); + assert_eq!(memory.read16(IF).unwrap(), 0); + assert_eq!(memory.cycles(), 0); + assert_eq!( + memory.write32(WAITCNT + 2, 0), + Err(MemoryError::Unaligned(WAITCNT + 2)) + ); + assert_eq!(memory.waitcnt(), 0x4317); +} + +#[test] +fn every_rom_wait_setting_has_the_documented_halfword_and_word_costs() { + for window in 0..3 { + for first in 0..4 { + for second in 0..2 { + let waitcnt = (first << [2, 5, 8][window]) | (second << [4, 7, 10][window]); + let n = [5, 4, 3, 9][first as usize]; + let s = if second == 1 { 2 } else { [3, 5, 9][window] }; + for mirror in [0, 0x0100_0000] { + let address = ROM_START + window as u32 * 0x0200_0000 + mirror + 0x100; + for (kind, initial) in + [(AccessKind::NonSequential, n), (AccessKind::Sequential, s)] + { + assert_eq!( + bus_cycles(waitcnt, address, AccessWidth::Byte, kind), + initial + ); + assert_eq!( + bus_cycles(waitcnt, address, AccessWidth::Halfword, kind), + initial + ); + assert_eq!( + bus_cycles(waitcnt, address, AccessWidth::Word, kind), + initial + s + ); + } + } + } + } + } +} + +#[test] +fn rom_128_kib_boundaries_force_nonsequential_first_halfwords() { + for address in [ + 0x0800_0000, + 0x0802_0000, + 0x09fe_0000, + 0x0a02_0000, + 0x0c02_0000, + ] { + let window = ((address >> 25) - 4) as usize; + let s = [3, 5, 9][window]; + assert_eq!( + bus_cycles(0, address, AccessWidth::Halfword, AccessKind::Sequential), + 5 + ); + assert_eq!( + bus_cycles(0, address + 1, AccessWidth::Byte, AccessKind::Sequential), + 5 + ); + assert_eq!( + bus_cycles(0, address, AccessWidth::Word, AccessKind::Sequential), + 5 + s + ); + assert_eq!( + bus_cycles( + 0, + address + 2, + AccessWidth::Halfword, + AccessKind::Sequential + ), + s + ); + } +} + +#[test] +fn internal_regions_and_ewram_use_fixed_width_specific_costs() { + for kind in [AccessKind::NonSequential, AccessKind::Sequential] { + for width in [AccessWidth::Byte, AccessWidth::Halfword, AccessWidth::Word] { + for address in [0, 0x0300_0000, 0x03ff_fffc, WAITCNT] { + assert_eq!(bus_cycles(0x5fff, address, width, kind), 1); + } + for address in [0x0200_0000, 0x02ff_fffc] { + assert_eq!( + bus_cycles(0x5fff, address, width, kind), + if width == AccessWidth::Word { 6 } else { 3 } + ); + } + } + } +} + +#[test] +fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() { + // Explicit limitation: no prefetch queue, PHI output, or SRAM mapping yet. + for width in [AccessWidth::Halfword, AccessWidth::Word] { + for address in [ROM_START, ROM_START + 4, 0x0a00_0004, 0x0c00_0004] { + for kind in [AccessKind::NonSequential, AccessKind::Sequential] { + assert_eq!( + bus_cycles(0, address, width, kind), + bus_cycles(0x5803, address, width, kind) + ); + } + } + } +} + +#[test] +fn wait_states_change_when_a_timer_interrupt_is_sampled() { + for (waitcnt, entry_step, instructions, cycles) in [(0, 4, 3, 23), (0x18, 6, 5, 24)] { + let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap(); + memory.write16(WAITCNT, waitcnt).unwrap(); + memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles + memory.write16(IE, 8).unwrap(); + memory.write32(IME, 1).unwrap(); + let mut machine = Machine::new(Cpu::new(ROM_START), memory); + assert_eq!(machine.last_timing().total(), 0); + for step in 1..=entry_step { + let event = machine.step().unwrap(); + assert_eq!( + event, + if step == entry_step { + StepKind::IrqEntry + } else { + StepKind::Instruction + } + ); + } + assert_eq!(machine.cpu().registers()[0], instructions); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.last_timing().code_cycles, 3); + } +} + +#[test] +fn failed_machine_step_preserves_previous_timing_breakdown() { + let mut memory = Memory::new(0xe1a0_0000_u32.to_le_bytes().to_vec()).unwrap(); + memory.write32(TIMER_BASE, 0x0080_0000).unwrap(); + let mut machine = Machine::new(Cpu::new(ROM_START), memory); + machine.step().unwrap(); + let timing = machine.last_timing(); + let cycles = machine.cycles(); + let counter = machine.memory().read16(TIMER_BASE).unwrap(); + assert!(machine.step().is_err()); + assert_eq!(machine.last_timing(), timing); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter); +}