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