feat(memory): retain sequential Thumb IWRAM bus lanes
Track a separate IWRAM latch with known lanes and transactional updates. Sequential Thumb prefetches and IWRAM byte/halfword/word reads and writes drive only addressed lanes. Other regions and host inspection do not replace the latch. Failed instructions discard staged changes. Keep cold, refill, discontinuous-entry, IRQ, and DMA-transition history conservative. Share Thumb refill classification with nominal timing, including PC writes to fallthrough. Successful DMA and accepted machine IRQs invalidate history; failed DMA preserves it. Document source evidence, bounded scope, and remaining hardware-validation limits. Add original lane, alignment, mirror/wrap, signed/unaligned-load, mode, block-transfer, rollback, isolation, control-flow, IRQ/DMA, and nominal-cost regressions. Darwin arm64 validation: 905 workspace tests and 836 core/demo tests per debug/release profile; 39 preparation tests; native ROM windows in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to prior passing reports and between profiles.
This commit is contained in:
parent
8360ba4652
commit
46af481487
17 changed files with 761 additions and 28 deletions
|
|
@ -22,6 +22,8 @@ mod exception_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod instruction_tests;
|
mod instruction_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod iwram_open_bus_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod load_alias_tests;
|
mod load_alias_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod open_bus_tests;
|
mod open_bus_tests;
|
||||||
|
|
@ -208,9 +210,15 @@ impl Cpu {
|
||||||
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
||||||
// Record the executing instruction, not an operand's pipelined PC value.
|
// Record the executing instruction, not an operand's pipelined PC value.
|
||||||
// Always clear the access context, including on diagnostic errors.
|
// Always clear the access context, including on diagnostic errors.
|
||||||
memory.begin_cpu_access(self.pc(), self.instruction_set);
|
let pc = self.pc();
|
||||||
|
let sequential_thumb =
|
||||||
|
self.instruction_set == InstructionSet::Thumb && !self.thumb_refills(instruction);
|
||||||
|
memory.begin_cpu_access(pc, self.instruction_set);
|
||||||
let result = self.execute_instruction(instruction, memory);
|
let result = self.execute_instruction(instruction, memory);
|
||||||
memory.end_cpu_access(result.is_ok());
|
let sequential = sequential_thumb
|
||||||
|
&& self.instruction_set == InstructionSet::Thumb
|
||||||
|
&& self.pc() == pc.wrapping_add(2);
|
||||||
|
memory.end_cpu_access(result.is_ok(), sequential);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
422
crates/gba-core/src/cpu/iwram_open_bus_tests.rs
Normal file
422
crates/gba-core/src/cpu/iwram_open_bus_tests.rs
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
//! Original sequential Thumb programs for the lane-retaining IWRAM bus.
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
dma::{DMA_BASE, DMA_STRIDE},
|
||||||
|
io::{IE, IME, TIMER_BASE},
|
||||||
|
machine::{Machine, StepKind},
|
||||||
|
memory::ROM_START,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CODE: u32 = 0x0300_0100;
|
||||||
|
const SOURCE: u32 = 0x0300_1000;
|
||||||
|
const OTHER: u32 = 0x0300_2000;
|
||||||
|
const UNUSED: u32 = 0x8000_0000;
|
||||||
|
const DATA: u32 = 0xf233_80a5;
|
||||||
|
const NEW_DATA: u32 = 0x7654_fedc;
|
||||||
|
const NOP: u16 = 0x46c0;
|
||||||
|
const SEED: u16 = 0x680a; // LDR r2,[r1]
|
||||||
|
const PROBE: u16 = 0x6806; // LDR r6,[r0]
|
||||||
|
|
||||||
|
fn program(pc: u32, code: &[u16]) -> (Cpu, Memory) {
|
||||||
|
let mut bus = Memory::new(NEW_DATA.to_le_bytes().to_vec()).unwrap();
|
||||||
|
for (index, instruction) in code.iter().enumerate() {
|
||||||
|
bus.write16(pc + index as u32 * 2, *instruction).unwrap();
|
||||||
|
}
|
||||||
|
bus.write32(SOURCE, DATA).unwrap();
|
||||||
|
bus.write32(OTHER, NEW_DATA).unwrap();
|
||||||
|
let mut cpu = Cpu::new(pc);
|
||||||
|
cpu.instruction_set = InstructionSet::Thumb;
|
||||||
|
cpu.registers[0] = UNUSED;
|
||||||
|
cpu.registers[1] = SOURCE;
|
||||||
|
cpu.registers[4] = OTHER;
|
||||||
|
(cpu, bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Independent byte-array reference: an IWRAM access replaces only addressed bytes.
|
||||||
|
fn drive(word: u32, address: u32, bytes: &[u8]) -> u32 {
|
||||||
|
let mut lanes = word.to_le_bytes();
|
||||||
|
let lane = (address & 3) as usize;
|
||||||
|
lanes[lane..lane + bytes.len()].copy_from_slice(bytes);
|
||||||
|
u32::from_le_bytes(lanes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequential_fetches_fill_alternating_lanes_and_keep_the_observed_old_halfword() {
|
||||||
|
for pc in [
|
||||||
|
CODE,
|
||||||
|
CODE + 2,
|
||||||
|
CODE + 0x8000,
|
||||||
|
CODE + 0x8002,
|
||||||
|
0x0300_7ffc,
|
||||||
|
0x0300_7ffe,
|
||||||
|
] {
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[NOP, PROBE, 0x1234, 0xabcd]);
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
// Host reads and writes do not drive the emulated bus. Do not reread P+2 as history.
|
||||||
|
assert_eq!(bus.read16(pc + 4).unwrap(), 0x1234);
|
||||||
|
bus.write16(pc + 4, 0xeeee).unwrap();
|
||||||
|
bus.write32(OTHER, 0xeeee_eeee).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6],
|
||||||
|
if pc & 2 == 0 {
|
||||||
|
0xabcd_1234
|
||||||
|
} else {
|
||||||
|
0x1234_abcd
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_load_seeds_history_before_cpu_rotation_and_current_fetch_replaces_one_half() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
for low in 0..4 {
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[SEED, PROBE, 0x1111, 0x9bcd]);
|
||||||
|
cpu.registers[1] += low;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(cpu.registers[2], DATA.rotate_right(low * 8));
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6],
|
||||||
|
if pc & 2 == 0 {
|
||||||
|
0x9bcd_80a5
|
||||||
|
} else {
|
||||||
|
0xf233_9bcd
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_history_supports_widths_sign_extension_and_unaligned_loads_in_all_modes() {
|
||||||
|
for mode in [
|
||||||
|
Mode::User,
|
||||||
|
Mode::System,
|
||||||
|
Mode::Fiq,
|
||||||
|
Mode::Irq,
|
||||||
|
Mode::Supervisor,
|
||||||
|
Mode::Abort,
|
||||||
|
Mode::Undefined,
|
||||||
|
] {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
let word: u32 = if pc & 2 == 0 {
|
||||||
|
0x9bcd_80a5
|
||||||
|
} else {
|
||||||
|
0xf233_9bcd
|
||||||
|
};
|
||||||
|
for (instruction, width, signed) in [
|
||||||
|
(0x6806, 4, false),
|
||||||
|
(0x7806, 1, false),
|
||||||
|
(0x8806, 2, false),
|
||||||
|
(0x5746, 1, true),
|
||||||
|
(0x5f46, 2, true), // r5=0 offset, r0 base, r6 result.
|
||||||
|
] {
|
||||||
|
for lane in 0..4 {
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[SEED, instruction, 0x1111, 0x9bcd]);
|
||||||
|
cpu.apply_status(0xb000_00e0 | mode as u32, mode);
|
||||||
|
cpu.registers[0] += lane;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
let byte = (word >> (lane * 8)) as u8;
|
||||||
|
let half = (word >> ((lane & 2) * 8)) as u16;
|
||||||
|
let value = match (width, signed) {
|
||||||
|
(4, _) => word.rotate_right(lane * 8),
|
||||||
|
(1, false) => u32::from(byte),
|
||||||
|
(1, true) => byte as i8 as i32 as u32,
|
||||||
|
(2, false) => u32::from(half).rotate_right((lane & 1) * 8),
|
||||||
|
(2, true) if lane & 1 != 0 => byte as i8 as i32 as u32,
|
||||||
|
(2, true) => half as i16 as i32 as u32,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let mut expected = cpu.clone();
|
||||||
|
expected.registers[6] = value;
|
||||||
|
expected.registers[15] += 2;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(cpu, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn byte_halfword_and_word_iwram_data_accesses_replace_only_addressed_lanes() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
for (instruction, width, store) in [
|
||||||
|
(0x7823, 1, false),
|
||||||
|
(0x8823, 2, false),
|
||||||
|
(0x6823, 4, false),
|
||||||
|
(0x5763, 1, false),
|
||||||
|
(0x5f63, 2, false), // Signed loads, offset r5=0.
|
||||||
|
(0x7023, 1, true),
|
||||||
|
(0x8023, 2, true),
|
||||||
|
(0x6023, 4, true),
|
||||||
|
] {
|
||||||
|
for lane in 0..4 {
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[SEED, instruction, PROBE, 0x5566, 0x99aa]);
|
||||||
|
cpu.registers[3] = 0x1234_abcd;
|
||||||
|
cpu.registers[4] += lane;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
let mut word = drive(DATA, pc + 6, &0x5566_u16.to_le_bytes());
|
||||||
|
let address = (OTHER + lane) & !(width - 1);
|
||||||
|
let start = (address & 3) as usize;
|
||||||
|
let value = if store {
|
||||||
|
0x1234_abcd_u32.to_le_bytes()
|
||||||
|
} else {
|
||||||
|
NEW_DATA.to_le_bytes()
|
||||||
|
};
|
||||||
|
// Odd LDRSH uses a byte, not a halfword, on ARM7.
|
||||||
|
let (address, start, width) = if instruction == 0x5f63 && lane & 1 != 0 {
|
||||||
|
(OTHER + lane, lane as usize, 1)
|
||||||
|
} else {
|
||||||
|
(address, start, width)
|
||||||
|
};
|
||||||
|
let bytes = if store {
|
||||||
|
&value[..width as usize]
|
||||||
|
} else {
|
||||||
|
&value[start..start + width as usize]
|
||||||
|
};
|
||||||
|
word = drive(word, address, bytes);
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
word = drive(word, pc + 8, &0x99aa_u16.to_le_bytes());
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6], word,
|
||||||
|
"pc={pc:#x} op={instruction:#x} lane={lane}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_iwram_data_reads_and_writes_do_not_replace_the_iwram_latch() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
for address in [
|
||||||
|
0x0200_0000,
|
||||||
|
0x0500_0000,
|
||||||
|
0x0600_0000,
|
||||||
|
0x0700_0000,
|
||||||
|
ROM_START,
|
||||||
|
] {
|
||||||
|
for instruction in [0x7823, 0x8823, 0x6823, 0x7023, 0x8023, 0x6023] {
|
||||||
|
if address == ROM_START && instruction & 0x0800 == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[SEED, instruction, PROBE, 0x5566, 0x99aa]);
|
||||||
|
if address != ROM_START {
|
||||||
|
bus.write32(address, NEW_DATA).unwrap();
|
||||||
|
}
|
||||||
|
cpu.registers[4] = address;
|
||||||
|
cpu.registers[3] = NEW_DATA;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
let word = drive(
|
||||||
|
drive(DATA, pc + 6, &0x5566_u16.to_le_bytes()),
|
||||||
|
pc + 8,
|
||||||
|
&0x99aa_u16.to_le_bytes(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6], word,
|
||||||
|
"address={address:#x} op={instruction:#x}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_transfers_leave_the_last_iwram_word_and_keep_writeback() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
for store in [false, true] {
|
||||||
|
let instruction = if store { 0xc40e } else { 0xcc0e }; // STM/LDM r4!,{r1-r3}
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[instruction, PROBE, 0x1111, 0x9bcd]);
|
||||||
|
for (index, value) in [DATA, NEW_DATA, 0x1357_2468].into_iter().enumerate() {
|
||||||
|
bus.write32(OTHER + index as u32 * 4, value).unwrap();
|
||||||
|
cpu.registers[index + 1] = value;
|
||||||
|
}
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(cpu.registers[4], OTHER + 12);
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6],
|
||||||
|
drive(0x1357_2468, pc + 6, &0x9bcd_u16.to_le_bytes())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cold_history_is_unknown_but_successful_fetches_or_word_accesses_can_establish_it() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[PROBE, PROBE, 0x1111, 0x2222]);
|
||||||
|
let before = cpu.clone();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step(&mut bus),
|
||||||
|
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||||
|
);
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
bus.write16(pc, NOP).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6],
|
||||||
|
if pc & 2 == 0 {
|
||||||
|
0x2222_1111
|
||||||
|
} else {
|
||||||
|
0x1111_2222
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_steps_discard_prefetch_history_before_retry() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
for instruction in [0xde00, 0x6823] {
|
||||||
|
// Undefined instruction / LDR from unsupported save memory.
|
||||||
|
let (mut cpu, mut bus) = program(pc, &[SEED, instruction, 0x1111, 0x9bcd]);
|
||||||
|
cpu.registers[4] = 0x0e00_0000;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
let before = cpu.clone();
|
||||||
|
assert!(cpu.step_timed(&mut bus).is_err());
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
bus.write16(pc + 2, PROBE).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.registers[6],
|
||||||
|
drive(DATA, pc + 6, &0x9bcd_u16.to_le_bytes())
|
||||||
|
);
|
||||||
|
assert_eq!(bus.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn taken_control_flow_invalidates_history_even_when_target_is_fallthrough() {
|
||||||
|
for branch in [0x4720, 0xe7ff, 0x46a7, 0xd0ff, 0xbd00] {
|
||||||
|
// BX, B, MOV pc, BEQ, POP pc.
|
||||||
|
let (mut cpu, mut bus) = program(CODE, &[SEED, branch, PROBE, 0x5566, 0x99aa]);
|
||||||
|
cpu.registers[4] = (CODE + 4) | 1;
|
||||||
|
cpu.registers[13] = OTHER;
|
||||||
|
bus.write32(OTHER, CODE + 4).unwrap();
|
||||||
|
cpu.flags.zero = true;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(cpu.pc(), CODE + 4);
|
||||||
|
let before = cpu.clone();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step(&mut bus),
|
||||||
|
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||||
|
);
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
}
|
||||||
|
let (mut cpu, mut bus) = program(CODE, &[SEED, 0xd0ff, PROBE, 0x5566, 0x99aa]);
|
||||||
|
cpu.flags.zero = false; // Untaken BEQ has no refill.
|
||||||
|
for _ in 0..3 {
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(cpu.registers[6], 0x5566_99aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pc_discontinuities_and_non_iwram_execution_do_not_reuse_old_history() {
|
||||||
|
let (mut cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x2222]);
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
bus.write16(CODE + 0x100, PROBE).unwrap();
|
||||||
|
cpu.registers[15] = CODE + 0x100;
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step(&mut bus),
|
||||||
|
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||||
|
);
|
||||||
|
bus.write16(0x0200_0000, NOP).unwrap();
|
||||||
|
cpu.registers[15] = 0x0200_0000;
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
cpu.registers[15] = CODE + 2;
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step(&mut bus),
|
||||||
|
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn successful_dma_invalidates_history_but_failed_dma_preserves_it() {
|
||||||
|
for fail in [false, true] {
|
||||||
|
let (cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
|
||||||
|
bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
machine.step().unwrap();
|
||||||
|
let cycles = machine.cycles();
|
||||||
|
let before = machine.cpu().clone();
|
||||||
|
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||||
|
machine
|
||||||
|
.memory_mut()
|
||||||
|
.write32(dma, if fail { 0x0e00_0000 } else { SOURCE })
|
||||||
|
.unwrap();
|
||||||
|
machine.memory_mut().write32(dma + 4, 0x0200_0000).unwrap();
|
||||||
|
machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap();
|
||||||
|
if fail {
|
||||||
|
assert!(machine.step().is_err());
|
||||||
|
assert_eq!(machine.cycles(), cycles);
|
||||||
|
machine.memory_mut().write16(dma + 10, 0).unwrap();
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[6], 0x9bcd_80a5);
|
||||||
|
} else {
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||||
|
assert_eq!(machine.cpu(), &before);
|
||||||
|
assert_eq!(machine.memory().read32(0x0200_0000).unwrap(), DATA);
|
||||||
|
let cycles = machine.cycles();
|
||||||
|
assert!(machine.step().is_err());
|
||||||
|
assert_eq!(machine.cycles(), cycles);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
|
||||||
|
machine.cycles()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_updates_add_no_data_accesses_or_device_cycles() {
|
||||||
|
for pc in [CODE, CODE + 2] {
|
||||||
|
let code = [SEED, PROBE, 0x1111, 0x9bcd];
|
||||||
|
let (cpu, mut bus) = program(pc, &code);
|
||||||
|
bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
let (mut untimed, mut bus) = program(pc, &code);
|
||||||
|
for _ in 0..2 {
|
||||||
|
untimed.step(&mut bus).unwrap();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.cpu(), &untimed);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 1);
|
||||||
|
assert_eq!(machine.last_timing().data_cycles, 1);
|
||||||
|
assert_eq!(machine.last_timing().internal_cycles, 1);
|
||||||
|
}
|
||||||
|
assert_eq!(machine.cycles(), 6);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6);
|
||||||
|
assert_eq!(bus.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepted_irq_invalidates_history_before_any_handler_instruction() {
|
||||||
|
let (cpu, bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
machine.step().unwrap();
|
||||||
|
let mut interrupted = machine.cpu().clone();
|
||||||
|
machine.memory_mut().write16(IE, 8).unwrap();
|
||||||
|
machine.memory_mut().write16(IME, 1).unwrap();
|
||||||
|
machine
|
||||||
|
.memory_mut()
|
||||||
|
.write32(TIMER_BASE, 0x00c0_ffff)
|
||||||
|
.unwrap();
|
||||||
|
machine.memory_mut().advance_cycles(1);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||||
|
// Probe the paused instruction with a separate CPU, without executing BIOS.
|
||||||
|
assert_eq!(
|
||||||
|
interrupted.step(machine.memory_mut()),
|
||||||
|
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -233,7 +233,7 @@ fn missing_bios_rom_save_and_unsupported_io_reads_remain_errors() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_iwram_thumb_reads_and_unused_fetches_do_not_reuse_arm_context() {
|
fn cold_iwram_thumb_reads_and_unused_fetches_do_not_reuse_arm_context() {
|
||||||
let mut bus = memory(LDR);
|
let mut bus = memory(LDR);
|
||||||
let mut cpu = Cpu::new(ROM_START);
|
let mut cpu = Cpu::new(ROM_START);
|
||||||
cpu.registers[0] = UNUSED;
|
cpu.registers[0] = UNUSED;
|
||||||
|
|
|
||||||
|
|
@ -238,12 +238,12 @@ fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn outside_thumb_execution_keeps_bios_history_without_enabling_iwram_open_bus() {
|
fn outside_thumb_execution_keeps_bios_history_without_seeding_iwram_history() {
|
||||||
let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5));
|
let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5));
|
||||||
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true);
|
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true);
|
||||||
cpu.step(&mut memory).unwrap();
|
cpu.step(&mut memory).unwrap();
|
||||||
assert_eq!(protected_word(&mut memory), OTHER);
|
assert_eq!(protected_word(&mut memory), OTHER);
|
||||||
memory.write16(0x0300_0100, 0x6801).unwrap(); // IWRAM needs unmodeled history.
|
memory.write16(0x0300_0100, 0x6801).unwrap(); // No preceding sequential IWRAM history.
|
||||||
cpu.registers[15] = 0x0300_0100;
|
cpu.registers[15] = 0x0300_0100;
|
||||||
cpu.registers[0] = 0x4000;
|
cpu.registers[0] = 0x4000;
|
||||||
let before = cpu.clone();
|
let before = cpu.clone();
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iwram_history_and_region_crossing_fetches_remain_diagnostics() {
|
fn cold_iwram_history_and_region_crossing_fetches_remain_diagnostics() {
|
||||||
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
|
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
|
||||||
for offset in [0, 2] {
|
for offset in [0, 2] {
|
||||||
let pc = base + offset;
|
let pc = base + offset;
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,12 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Share the existing control-flow classification with bounded bus history.
|
||||||
|
// A PC write can refill even when its target equals the sequential address.
|
||||||
|
pub(super) fn thumb_refills(&self, instruction: u32) -> bool {
|
||||||
|
self.thumb_summary(instruction).refill
|
||||||
|
}
|
||||||
|
|
||||||
fn thumb_summary(&self, instruction: u32) -> Summary {
|
fn thumb_summary(&self, instruction: u32) -> Summary {
|
||||||
match instruction {
|
match instruction {
|
||||||
0x4000..=0x43ff => {
|
0x4000..=0x43ff => {
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,7 @@ impl Machine {
|
||||||
return Ok(StepKind::HaltIdle);
|
return Ok(StepKind::HaltIdle);
|
||||||
}
|
}
|
||||||
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
|
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
|
||||||
|
self.memory.invalidate_cpu_bus_history();
|
||||||
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
|
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use std::{cell::Cell, error::Error, fmt};
|
use std::{cell::Cell, error::Error, fmt};
|
||||||
|
|
||||||
|
mod iwram_bus;
|
||||||
|
use iwram_bus::IwramBus;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cpu::InstructionSet,
|
cpu::InstructionSet,
|
||||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
|
|
@ -62,7 +65,8 @@ struct CpuAccess {
|
||||||
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
|
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
|
||||||
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
|
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
|
||||||
/// Protected BIOS reads separately retain the snapshot from BIOS execution.
|
/// Protected BIOS reads separately retain the snapshot from BIOS execution.
|
||||||
/// Thumb IWRAM history, a full fetch pipeline, and DMA bus latches are not modeled.
|
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally.
|
||||||
|
/// Refill history, a full fetch pipeline, and DMA bus latches are not modeled.
|
||||||
pub struct Memory {
|
pub struct Memory {
|
||||||
external_ram: Vec<u8>,
|
external_ram: Vec<u8>,
|
||||||
internal_ram: Vec<u8>,
|
internal_ram: Vec<u8>,
|
||||||
|
|
@ -76,6 +80,7 @@ pub struct Memory {
|
||||||
data_timing: Cell<Option<DataTiming>>,
|
data_timing: Cell<Option<DataTiming>>,
|
||||||
cpu_access: Option<CpuAccess>,
|
cpu_access: Option<CpuAccess>,
|
||||||
bios_prefetch: Option<u32>,
|
bios_prefetch: Option<u32>,
|
||||||
|
iwram_bus: IwramBus,
|
||||||
scanline_capture: Option<video::capture::Capture>,
|
scanline_capture: Option<video::capture::Capture>,
|
||||||
sprite_pipeline: pipeline::SpritePipeline,
|
sprite_pipeline: pipeline::SpritePipeline,
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +103,7 @@ impl Memory {
|
||||||
data_timing: Cell::new(None),
|
data_timing: Cell::new(None),
|
||||||
cpu_access: None,
|
cpu_access: None,
|
||||||
bios_prefetch: None,
|
bios_prefetch: None,
|
||||||
|
iwram_bus: IwramBus::default(),
|
||||||
scanline_capture: None,
|
scanline_capture: None,
|
||||||
sprite_pipeline: pipeline::SpritePipeline::default(),
|
sprite_pipeline: pipeline::SpritePipeline::default(),
|
||||||
})
|
})
|
||||||
|
|
@ -306,6 +312,8 @@ impl Memory {
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
}
|
}
|
||||||
.map_err(map_error)?;
|
.map_err(map_error)?;
|
||||||
|
// DMA-to-CPU bus ordering is not modeled. Never reuse pre-DMA IWRAM history.
|
||||||
|
self.invalidate_cpu_bus_history();
|
||||||
// Keep this channel active during its cycles: another edge must not
|
// Keep this channel active during its cycles: another edge must not
|
||||||
// queue a second block while the current block is still transferring.
|
// queue a second block while the current block is still transferring.
|
||||||
self.advance_cycles(timing.total());
|
self.advance_cycles(timing.total());
|
||||||
|
|
@ -332,6 +340,11 @@ impl Memory {
|
||||||
debug_assert!(self.cpu_access.is_none());
|
debug_assert!(self.cpu_access.is_none());
|
||||||
// Sample before execution, without data timing or open-bus recursion.
|
// Sample before execution, without data timing or open-bus recursion.
|
||||||
// Missing lookahead bytes must not fail an instruction that never uses them.
|
// Missing lookahead bytes must not fail an instruction that never uses them.
|
||||||
|
let iwram_pc = (instruction_set == InstructionSet::Thumb && pc >> 24 == 3).then_some(pc);
|
||||||
|
let fetched = iwram_pc
|
||||||
|
.filter(|pc| pc.wrapping_add(4) >> 24 == 3)
|
||||||
|
.and_then(|pc| self.snapshot_mapped_halfword(pc + 4));
|
||||||
|
self.iwram_bus.begin(iwram_pc, fetched);
|
||||||
let prefetch = match instruction_set {
|
let prefetch = match instruction_set {
|
||||||
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
|
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
|
||||||
InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
|
InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
|
||||||
|
|
@ -348,20 +361,24 @@ impl Memory {
|
||||||
match pc >> 24 {
|
match pc >> 24 {
|
||||||
// BIOS and OAM drive a full word even for a halfword instruction fetch.
|
// BIOS and OAM drive a full word even for a halfword instruction fetch.
|
||||||
0x00 | 0x07 => self.snapshot_mapped_word(address & !3),
|
0x00 | 0x07 => self.snapshot_mapped_word(address & !3),
|
||||||
|
// The IWRAM halfword fetch replaces one lane of its retained bus word.
|
||||||
|
0x03 => self.iwram_bus.snapshot(),
|
||||||
// These 16-bit regions repeat the fetched halfword in both word lanes.
|
// These 16-bit regions repeat the fetched halfword in both word lanes.
|
||||||
0x02 | 0x05 | 0x06 | 0x08..=0x0d => {
|
0x02 | 0x05 | 0x06 | 0x08..=0x0d => {
|
||||||
let half = u16::from_le_bytes([
|
Some(u32::from(self.snapshot_mapped_halfword(address)?) * 0x0001_0001)
|
||||||
self.read_mapped_byte(address).ok()?,
|
|
||||||
self.read_mapped_byte(address + 1).ok()?,
|
|
||||||
]);
|
|
||||||
Some(u32::from(half) * 0x0001_0001)
|
|
||||||
}
|
}
|
||||||
// IWRAM needs the previous bus value, including intervening data loads/DMA.
|
|
||||||
// Unsupported code regions must not manufacture an open-bus snapshot.
|
// Unsupported code regions must not manufacture an open-bus snapshot.
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn snapshot_mapped_halfword(&self, address: u32) -> Option<u16> {
|
||||||
|
Some(u16::from_le_bytes([
|
||||||
|
self.read_mapped_byte(address).ok()?,
|
||||||
|
self.read_mapped_byte(address + 1).ok()?,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
|
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
|
||||||
let mut bytes = [0; 4];
|
let mut bytes = [0; 4];
|
||||||
for (offset, byte) in bytes.iter_mut().enumerate() {
|
for (offset, byte) in bytes.iter_mut().enumerate() {
|
||||||
|
|
@ -372,7 +389,8 @@ impl Memory {
|
||||||
Some(u32::from_le_bytes(bytes))
|
Some(u32::from_le_bytes(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn end_cpu_access(&mut self, succeeded: bool) {
|
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) {
|
||||||
|
self.iwram_bus.finish(succeeded, sequential);
|
||||||
if let Some(access) = self.cpu_access.take() {
|
if let Some(access) = self.cpu_access.take() {
|
||||||
if succeeded && access.pc < BIOS_SIZE as u32 {
|
if succeeded && access.pc < BIOS_SIZE as u32 {
|
||||||
// Commit only successful instruction snapshots under our diagnostic policy.
|
// Commit only successful instruction snapshots under our diagnostic policy.
|
||||||
|
|
@ -382,6 +400,10 @@ impl Memory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn invalidate_cpu_bus_history(&mut self) {
|
||||||
|
self.iwram_bus.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
fn can_write_power_control(&self) -> bool {
|
fn can_write_power_control(&self) -> bool {
|
||||||
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
|
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
|
||||||
self.cpu_access
|
self.cpu_access
|
||||||
|
|
@ -442,6 +464,8 @@ impl Memory {
|
||||||
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
|
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
|
||||||
let value = self.read_byte(address)?;
|
let value = self.read_byte(address)?;
|
||||||
self.record_access(address, AccessWidth::Byte);
|
self.record_access(address, AccessWidth::Byte);
|
||||||
|
self.iwram_bus
|
||||||
|
.access(address, AccessWidth::Byte, u32::from(value));
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,6 +544,21 @@ impl Memory {
|
||||||
_ => unreachable!("unsupported bus width"),
|
_ => unreachable!("unsupported bus width"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if address >> 24 == 0x03 {
|
||||||
|
let (width, value) = match N {
|
||||||
|
1 => (AccessWidth::Byte, u32::from(bytes[0])),
|
||||||
|
2 => (
|
||||||
|
AccessWidth::Halfword,
|
||||||
|
u32::from(u16::from_le_bytes([bytes[0], bytes[1]])),
|
||||||
|
),
|
||||||
|
4 => (
|
||||||
|
AccessWidth::Word,
|
||||||
|
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||||
|
),
|
||||||
|
_ => unreachable!("unsupported bus width"),
|
||||||
|
};
|
||||||
|
self.iwram_bus.access(address, width, value);
|
||||||
|
}
|
||||||
if address >> 24 == 0x04 {
|
if address >> 24 == 0x04 {
|
||||||
if address & !3 == KEYINPUT {
|
if address & !3 == KEYINPUT {
|
||||||
self.io.write_keypad(address, &bytes);
|
self.io.write_keypad(address, &bytes);
|
||||||
|
|
@ -615,6 +654,8 @@ impl Memory {
|
||||||
}
|
}
|
||||||
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
let value = u16::from_le_bytes([self.read_byte(address)?, self.read_byte(address + 1)?]);
|
||||||
self.record_access(address, AccessWidth::Halfword);
|
self.record_access(address, AccessWidth::Halfword);
|
||||||
|
self.iwram_bus
|
||||||
|
.access(address, AccessWidth::Halfword, u32::from(value));
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -630,6 +671,7 @@ impl Memory {
|
||||||
self.read_byte(address + 3)?,
|
self.read_byte(address + 3)?,
|
||||||
]);
|
]);
|
||||||
self.record_access(address, AccessWidth::Word);
|
self.record_access(address, AccessWidth::Word);
|
||||||
|
self.iwram_bus.access(address, AccessWidth::Word, value);
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
crates/gba-core/src/memory/iwram_bus.rs
Normal file
130
crates/gba-core/src/memory/iwram_bus.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
//! Transactional lane history for consecutive Thumb instructions in IWRAM.
|
||||||
|
//! Unknown startup/refill/DMA history is not replaced with current memory bytes.
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use crate::timing::AccessWidth;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct Lanes {
|
||||||
|
value: u32,
|
||||||
|
known: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lanes {
|
||||||
|
fn drive(&mut self, address: u32, width: AccessWidth, value: u32) {
|
||||||
|
debug_assert_eq!(address & (width.bytes() - 1), 0);
|
||||||
|
let mask = match width {
|
||||||
|
AccessWidth::Byte => 0xff,
|
||||||
|
AccessWidth::Halfword => 0xffff,
|
||||||
|
AccessWidth::Word => u32::MAX,
|
||||||
|
};
|
||||||
|
let shift = (address & 3) * 8;
|
||||||
|
let mask = mask << shift;
|
||||||
|
self.value = (self.value & !mask) | ((value << shift) & mask);
|
||||||
|
self.known |= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word(self) -> Option<u32> {
|
||||||
|
(self.known == u32::MAX).then_some(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct History {
|
||||||
|
next_pc: u32,
|
||||||
|
lanes: Lanes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct IwramBus {
|
||||||
|
committed: Option<History>,
|
||||||
|
// Reads use &Memory. Stage their lane changes without changing committed history.
|
||||||
|
pending: Cell<Option<History>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IwramBus {
|
||||||
|
pub(super) fn begin(&mut self, pc: Option<u32>, fetched: Option<u16>) {
|
||||||
|
debug_assert!(self.pending.get().is_none());
|
||||||
|
self.pending.set(pc.map(|pc| {
|
||||||
|
let mut lanes = self
|
||||||
|
.committed
|
||||||
|
.filter(|history| history.next_pc == pc)
|
||||||
|
.map_or_else(Lanes::default, |history| history.lanes);
|
||||||
|
if let Some(half) = fetched {
|
||||||
|
lanes.drive(pc + 4, AccessWidth::Halfword, u32::from(half));
|
||||||
|
} else {
|
||||||
|
lanes = Lanes::default(); // Unavailable or region-crossing prefetch.
|
||||||
|
}
|
||||||
|
History {
|
||||||
|
next_pc: pc.wrapping_add(2),
|
||||||
|
lanes,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn snapshot(&self) -> Option<u32> {
|
||||||
|
self.pending.get().and_then(|history| history.lanes.word())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn access(&self, address: u32, width: AccessWidth, value: u32) {
|
||||||
|
if address >> 24 != 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(mut history) = self.pending.get() {
|
||||||
|
history.lanes.drive(address, width, value);
|
||||||
|
self.pending.set(Some(history));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn finish(&mut self, succeeded: bool, sequential: bool) {
|
||||||
|
let pending = self.pending.take();
|
||||||
|
if succeeded {
|
||||||
|
self.committed = if sequential {
|
||||||
|
pending.filter(|history| history.next_pc >> 24 == 3)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn invalidate(&mut self) {
|
||||||
|
debug_assert!(self.pending.get().is_none());
|
||||||
|
self.committed = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incomplete_lanes_stay_unknown_until_all_bytes_have_been_driven() {
|
||||||
|
let mut bus = IwramBus::default();
|
||||||
|
bus.begin(Some(0x0300_0100), Some(0x1234));
|
||||||
|
assert_eq!(bus.snapshot(), None);
|
||||||
|
bus.access(0x0300_2002, AccessWidth::Byte, 0x56);
|
||||||
|
assert_eq!(bus.snapshot(), None);
|
||||||
|
bus.access(0x0200_2003, AccessWidth::Byte, 0xff); // Other memory cannot fill a lane.
|
||||||
|
assert_eq!(bus.snapshot(), None);
|
||||||
|
bus.access(0x0300_2003, AccessWidth::Byte, 0x78);
|
||||||
|
assert_eq!(bus.snapshot(), Some(0x7856_1234));
|
||||||
|
bus.finish(true, true);
|
||||||
|
bus.begin(Some(0x0300_0102), Some(0xabcd));
|
||||||
|
assert_eq!(bus.snapshot(), Some(0xabcd_1234));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_transaction_discards_all_staged_accesses_not_only_prefetch() {
|
||||||
|
let mut bus = IwramBus::default();
|
||||||
|
bus.begin(Some(0x0300_0100), Some(0x1234));
|
||||||
|
bus.access(0x0300_2000, AccessWidth::Word, 0x1122_3344);
|
||||||
|
bus.finish(true, true);
|
||||||
|
bus.begin(Some(0x0300_0102), Some(0xabcd));
|
||||||
|
bus.access(0x0300_2000, AccessWidth::Word, 0xdead_beef);
|
||||||
|
bus.access(0x0300_2001, AccessWidth::Byte, 0x55);
|
||||||
|
assert_eq!(bus.snapshot(), Some(0xdead_55ef));
|
||||||
|
bus.finish(false, false);
|
||||||
|
bus.begin(Some(0x0300_0102), Some(0x5678));
|
||||||
|
assert_eq!(bus.snapshot(), Some(0x5678_3344));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,8 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics |
|
| `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics |
|
||||||
| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing |
|
| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing |
|
||||||
| `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing |
|
| `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing |
|
||||||
|
| `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Sequential IWRAM lane history, access widths, isolation, errors, refills, IRQ/DMA invalidation, and nominal timing |
|
||||||
|
| `crates/gba-core/src/memory/iwram_bus.rs` | Transactional known-lane history for sequential Thumb IWRAM execution |
|
||||||
| `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions |
|
| `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions |
|
||||||
| `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests |
|
| `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests |
|
||||||
| `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks |
|
| `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks |
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,12 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no
|
||||||
|
|
||||||
Limits remain explicit:
|
Limits remain explicit:
|
||||||
|
|
||||||
- Thumb open bus uses the supported region rules below. Internal work RAM still needs unmodeled bus history.
|
- Thumb open bus uses the supported region rules below, including bounded sequential internal-RAM lane history.
|
||||||
- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots.
|
- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots.
|
||||||
- Unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here.
|
- Unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here.
|
||||||
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
|
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
|
||||||
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write.
|
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write.
|
||||||
- No persistent pipeline or bus history exists. Self-modifying code and DMA-to-CPU bus transitions are not hardware-accurate.
|
- There is no persistent instruction pipeline. Sequential Thumb IWRAM history does not model refills or DMA-to-CPU bus transitions.
|
||||||
|
|
||||||
This subset lets public ARM test 362 complete. That test does not assert the loaded data value.
|
This subset lets public ARM test 362 complete. That test does not assert the loaded data value.
|
||||||
Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md).
|
Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md).
|
||||||
|
|
@ -116,7 +116,7 @@ The snapshot depends on the executing code region. P below is the executing inst
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes |
|
| External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes |
|
||||||
| BIOS and object attribute memory (OAM) | Full word at `(P+4) & ~3` |
|
| BIOS and object attribute memory (OAM) | Full word at `(P+4) & ~3` |
|
||||||
| Internal work RAM (IWRAM) | Unsupported: one lane depends on previous bus activity |
|
| Internal work RAM (IWRAM) | PC+4 replaces one halfword in known sequential IWRAM history; the other lanes retain their values |
|
||||||
| Other code regions | Unsupported |
|
| Other code regions | Unsupported |
|
||||||
|
|
||||||
For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6.
|
For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6.
|
||||||
|
|
@ -135,14 +135,39 @@ Snapshots add no nominal cycles. Host reads, instruction fetches, DMA, and unsup
|
||||||
Protected BIOS reads use separate retained history, not the current ROM/RAM snapshot.
|
Protected BIOS reads use separate retained history, not the current ROM/RAM snapshot.
|
||||||
|
|
||||||
GBATEK and nocash's [open-bus findings](https://www.ngemu.com/threads/gba-open-bus.170809/) document these region differences.
|
GBATEK and nocash's [open-bus findings](https://www.ngemu.com/threads/gba-open-bus.170809/) document these region differences.
|
||||||
IWRAM needs prior bus lanes, including data-load overwrites and possible DMA effects.
|
IWRAM needs prior bus lanes, including IWRAM data-read/write changes and possible DMA effects.
|
||||||
Do not substitute P+2 for that history: it is only the usual case, not a general rule.
|
Do not substitute P+2 for that history: it is only the usual case, not a general rule.
|
||||||
The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA.
|
The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA.
|
||||||
|
|
||||||
This remains a bounded snapshot implementation, not a pipeline or a complete bus-history model.
|
This remains a bounded snapshot implementation, not a pipeline or a complete bus-history model.
|
||||||
Refill timing, self-modifying code, DMA-to-CPU transitions, IWRAM, unused/write-only I/O, and disabled RAM remain incomplete.
|
Refill timing, self-modifying code, DMA-to-CPU transitions, cross-state IWRAM history, unused/write-only I/O, and disabled RAM remain incomplete.
|
||||||
Original regressions validate the documented formulas. No physical-hardware or independent public Thumb open-bus pass is claimed.
|
Original regressions validate the documented formulas. No physical-hardware or independent public Thumb open-bus pass is claimed.
|
||||||
|
|
||||||
|
### Sequential IWRAM history
|
||||||
|
|
||||||
|
For consecutive Thumb instructions in IWRAM, the emulator retains a separate lane value and known-bit mask.
|
||||||
|
The PC+4 halfword fetch updates its addressed halfword before execution.
|
||||||
|
IWRAM data reads and writes then update only their addressed byte, halfword, or word lanes.
|
||||||
|
The recorded value is raw aligned bus data, before CPU rotation or sign extension.
|
||||||
|
Accesses to other regions do not replace the IWRAM latch.
|
||||||
|
See [the evidence and implementation scope](../research/iwram-bus-history.md).
|
||||||
|
|
||||||
|
History starts unknown. An unused-memory load requires all word lanes to be known, including for byte/halfword loads.
|
||||||
|
Ordinary instructions and mapped loads still execute with incomplete history.
|
||||||
|
Consecutive fetches can establish both halves. An IWRAM word read or write establishes the entire word.
|
||||||
|
Host inspection and setup writes do not seed or change this emulated history.
|
||||||
|
|
||||||
|
Each instruction stages its fetch and data-access changes.
|
||||||
|
Success commits history only for the next sequential Thumb IWRAM PC. Failure preserves the previous committed history.
|
||||||
|
Block-transfer data accesses stage updates in order; unused-memory reads still use one entry snapshot per instruction.
|
||||||
|
This preserves the existing atomic diagnostic policy without adding bus or device cycles.
|
||||||
|
|
||||||
|
Taken branches and PC-writing refills invalidate continuation history, even when their target is the fallthrough address.
|
||||||
|
Untaken conditional branches keep sequential history.
|
||||||
|
Other instruction states/regions, accepted machine IRQs, and successful DMA units end the supported history sequence.
|
||||||
|
Failed DMA units leave history unchanged. Discontinuous entry starts unknown instead of borrowing stale lanes.
|
||||||
|
Refill fetches, ARM-to-Thumb history, and DMA latch contents remain unmodeled.
|
||||||
|
|
||||||
## Processor status and exceptions
|
## Processor status and exceptions
|
||||||
|
|
||||||
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.
|
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,5 @@ DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifyi
|
||||||
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
|
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
|
||||||
Unmapped memory and cartridge writes remain diagnostics, not hardware open-bus/latch behavior.
|
Unmapped memory and cartridge writes remain diagnostics, not hardware open-bus/latch behavior.
|
||||||
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
|
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
|
||||||
|
Successful units invalidate bounded sequential Thumb IWRAM history because DMA-to-CPU latch ordering is not modeled.
|
||||||
|
Failed units preserve that history. No DMA value is substituted for a CPU open-bus read.
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ Hardware references used for the core:
|
||||||
- [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model.
|
- [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model.
|
||||||
- [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits.
|
- [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits.
|
||||||
- [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12.
|
- [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12.
|
||||||
- [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, the distinct Thumb region/history rules, and PC-dependent BIOS read protection. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas; IWRAM and region-transition history remain unsupported.
|
- [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, the distinct Thumb region/history rules, and PC-dependent BIOS read protection. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas. IWRAM has bounded sequential lane history; refills and region transitions remain unsupported.
|
||||||
- [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for ARM prefetch, repeated Thumb halfwords in 16-bit regions, aligned BIOS/OAM words, and history-dependent IWRAM lanes. Its data-load overwrite note prevents treating P+2 as a universal IWRAM rule.
|
- [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for ARM prefetch, repeated Thumb halfwords in 16-bit regions, aligned BIOS/OAM words, and history-dependent IWRAM lanes. Its data-load overwrite note prevents treating P+2 as a universal IWRAM rule.
|
||||||
- [mGBA memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c), reviewed for `GBALoadBad`, ARM prefetch sourcing, byte/halfword lane selection, and separate DMA/history behavior. Also reviewed its BIOS region-exit prefetch retention and protected-load lane selection.
|
- [mGBA memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c), reviewed for `GBALoadBad`, ARM prefetch sourcing, byte/halfword lane selection, and separate DMA/history behavior. Also reviewed its BIOS region-exit prefetch retention and protected-load lane selection.
|
||||||
- [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm).
|
- [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm).
|
||||||
|
|
@ -104,6 +104,14 @@ Thumb BIOS fetch research:
|
||||||
|
|
||||||
See [Thumb BIOS prefetch snapshots](research/thumb-bios-prefetch.md) for the inference, implementation limits, and original regressions.
|
See [Thumb BIOS prefetch snapshots](research/thumb-bios-prefetch.md) for the inference, implementation limits, and original regressions.
|
||||||
|
|
||||||
|
IWRAM lane-history research:
|
||||||
|
|
||||||
|
- [mGBA issue 1575](https://github.com/mgba-emu/mgba/issues/1575), for reported hardware alignment results and IWRAM read/write effects.
|
||||||
|
- [ares IWRAM accesses](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) and [bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp), for separate byte-lane retention and debugger isolation.
|
||||||
|
- [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) and [its correction](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195), for distinguishing the IWRAM latch from reads to other regions.
|
||||||
|
|
||||||
|
See [IWRAM Thumb bus history](research/iwram-bus-history.md). These were source reviews, not local hardware or external-emulator test runs.
|
||||||
|
|
||||||
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
||||||
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
||||||
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
||||||
|
|
|
||||||
79
docs/research/iwram-bus-history.md
Normal file
79
docs/research/iwram-bus-history.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# IWRAM Thumb bus history
|
||||||
|
|
||||||
|
## Finding
|
||||||
|
|
||||||
|
Internal work RAM (IWRAM) retains its own bus lanes. A previous CPU load result is not a universal replacement.
|
||||||
|
An IWRAM byte, halfword, or word access updates only the addressed lanes of that latch.
|
||||||
|
A Thumb fetch updates one halfword. Reads and writes elsewhere do not replace the IWRAM latch.
|
||||||
|
This distinction matters when Thumb code reads another memory region before an unused-memory read.
|
||||||
|
|
||||||
|
For sequential Thumb execution at P, the fetch at P+4 updates:
|
||||||
|
|
||||||
|
- Bits 0–15 when P is word-aligned.
|
||||||
|
- Bits 16–31 when P has only halfword alignment.
|
||||||
|
|
||||||
|
The other halfword retains its previous IWRAM value.
|
||||||
|
Without intervening IWRAM data accesses, consecutive fetches produce GBATEK's usual P+2/P+4 combination.
|
||||||
|
Using current bytes at P+2 would lose observed history and ignore data-access changes.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- [GBATEK](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm) documents the two Thumb IWRAM lanes and warns about overwritten history.
|
||||||
|
- [mGBA issue 1575](https://github.com/mgba-emu/mgba/issues/1575) reports hardware-based alignment expectations and read/write effects on IWRAM open bus.
|
||||||
|
- [ares IWRAM access implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) retains separate IWRAM lanes for byte, halfword, and word reads and writes.
|
||||||
|
Its [bus implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) distinguishes this latch from the general data bus and debugger access.
|
||||||
|
- [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) identifies failures after byte/halfword reads from other regions.
|
||||||
|
The [correction at fab6e2cc](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195) adds a separate IWRAM latch.
|
||||||
|
The commit reports that its openbuster tests pass. That report is upstream evidence, not a test run by this project.
|
||||||
|
|
||||||
|
These sources support a separate lane-retaining latch, rather than a copy of the last CPU destination register.
|
||||||
|
Our implementation and synthetic programs are original. No external source code or test ROM was imported.
|
||||||
|
No physical-hardware measurement or external emulator differential run was performed here.
|
||||||
|
|
||||||
|
## Bounded implementation
|
||||||
|
|
||||||
|
`memory/iwram_bus.rs` records lane values and a known-bit mask during consecutive Thumb instructions in IWRAM.
|
||||||
|
Before execution, the mapped PC+4 halfword updates its addressed lanes.
|
||||||
|
During execution, successful IWRAM bus reads and writes update the staged lanes at their actual aligned addresses and widths.
|
||||||
|
The latch sees raw bus data before CPU load rotation and sign extension.
|
||||||
|
Other-region accesses and host inspection do not change it.
|
||||||
|
|
||||||
|
A completed sequential instruction commits the staged history for the next exact virtual PC.
|
||||||
|
A failed instruction discards all staged changes and preserves the last committed history.
|
||||||
|
The existing whole-instruction diagnostic policy still applies; this is not hardware data-abort behavior.
|
||||||
|
All unused-memory reads within one instruction use its entry snapshot.
|
||||||
|
|
||||||
|
The model starts with unknown lanes. An unused-memory load requires a complete known word, even for a narrower load.
|
||||||
|
Ordinary instructions and mapped data accesses can establish history without requiring a prior complete word.
|
||||||
|
Consecutive halfword fetches or an IWRAM word access can make every lane known.
|
||||||
|
Missing lookahead and 16 MiB region-crossing fetches retain diagnostics.
|
||||||
|
|
||||||
|
Successful branches and other refill instructions invalidate continuation history, even when the target equals the next sequential address.
|
||||||
|
The existing Thumb timing classifier identifies these refills for both timed and untimed CPU stepping.
|
||||||
|
Execution outside Thumb IWRAM also ends this bounded history sequence.
|
||||||
|
An accepted machine IRQ and each successful DMA unit invalidate the history. Failed DMA units preserve it.
|
||||||
|
The implementation does not guess refill or DMA latch contents.
|
||||||
|
|
||||||
|
This is not a full IWRAM bus model across all CPU states. ARM access history, refill fetches, and DMA-to-CPU ordering remain incomplete.
|
||||||
|
It is not a persistent instruction pipeline. Self-modifying instruction execution and exact per-access device timing remain unverified.
|
||||||
|
|
||||||
|
## Original regression coverage
|
||||||
|
|
||||||
|
- Both code alignments, IWRAM mirrors, and physical RAM wrap.
|
||||||
|
- Captured halfwords that remain unchanged by host inspection or later host writes.
|
||||||
|
- Byte/halfword/word data reads and writes, sign extension, load rotation, and all processor modes.
|
||||||
|
- Other-region accesses that leave IWRAM lanes unchanged.
|
||||||
|
- Multiple-register transfers, final-word retention, and writeback.
|
||||||
|
- Unknown lanes, establishment of known history, staged-access rollback, and retry.
|
||||||
|
- Taken and untaken branches, PC writes to fallthrough, discontinuities, IRQ entry, and DMA success/failure.
|
||||||
|
- Equal timed/untimed CPU results and unchanged nominal data/device costs.
|
||||||
|
|
||||||
|
## Validation result
|
||||||
|
|
||||||
|
The sequential-history regressions reproduced the old unsupported-read failures before implementation.
|
||||||
|
Workspace and core/demo tests pass on Darwin arm64 in debug and release builds.
|
||||||
|
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes also pass.
|
||||||
|
Public ARM, Thumb, memory, and BIOS reports match their previous passing results exactly.
|
||||||
|
Debug and release reports also match for the same fixture paths.
|
||||||
|
|
||||||
|
Those public suites remain regression checks, not independent IWRAM-history conformance tests.
|
||||||
|
|
@ -62,7 +62,7 @@ Original tests in `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` cover:
|
||||||
- Sequential history, ARM/Thumb transitions, SWI entry, and POP-to-PC exits with RAM stack data.
|
- Sequential history, ARM/Thumb transitions, SWI entry, and POP-to-PC exits with RAM stack data.
|
||||||
- Raw reads inside BIOS and protected reads outside it.
|
- Raw reads inside BIOS and protected reads outside it.
|
||||||
- The last complete fetch word, unavailable lookahead, and failure retention.
|
- The last complete fetch word, unavailable lookahead, and failure retention.
|
||||||
- Separate host reads, retained IWRAM Thumb open-bus diagnostics, nominal costs, and device progression.
|
- Separate host reads, retained cold-IWRAM Thumb open-bus diagnostics, nominal costs, and device progression.
|
||||||
|
|
||||||
## Validation result
|
## Validation result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill.
|
- ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill.
|
||||||
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation.
|
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation.
|
||||||
- Thumb unused-memory reads use repeated PC+4 halfwords in 16-bit code regions and aligned PC+4 words in BIOS/OAM.
|
- Thumb unused-memory reads use repeated PC+4 halfwords in 16-bit code regions and aligned PC+4 words in BIOS/OAM.
|
||||||
|
- Sequential Thumb IWRAM history retains fetched and data-access lanes, with known-bit tracking and diagnostic rollback.
|
||||||
- Little-endian byte, halfword, and word reads/writes.
|
- Little-endian byte, halfword, and word reads/writes.
|
||||||
- ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior.
|
- ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior.
|
||||||
- Explicit errors for unsupported instructions when their condition passes, and for unsupported memory accesses.
|
- Explicit errors for unsupported instructions when their condition passes, and for unsupported memory accesses.
|
||||||
|
|
@ -131,8 +132,10 @@ Other public suites remain unverified. These results do not establish full CPU o
|
||||||
The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
|
The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
|
||||||
|
|
||||||
ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads).
|
ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads).
|
||||||
Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) where the documented value needs no prior bus history.
|
Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and bounded sequential IWRAM lane history.
|
||||||
This is not a fetch pipeline. Thumb IWRAM, region-crossing fetches, DMA, and unused/write-only I/O open bus remain unsupported.
|
Cold or invalidated IWRAM history remains diagnostic until all lanes are known.
|
||||||
|
Refill instructions, accepted machine IRQs, successful DMA, and execution outside Thumb IWRAM end the supported history sequence.
|
||||||
|
This is not a fetch pipeline. Refill history, region-crossing fetches, DMA latches, and unused/write-only I/O open bus remain unsupported.
|
||||||
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
|
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
|
||||||
Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
|
Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
|
||||||
Reads beyond the supplied cartridge bytes also return errors.
|
Reads beyond the supplied cartridge bytes also return errors.
|
||||||
|
|
@ -142,8 +145,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
1. Add validated prior-bus history for Thumb IWRAM open bus, including data-load overwrites and DMA transitions.
|
1. Extend IWRAM history across validated refill and DMA-to-CPU transitions.
|
||||||
Keep unknown history diagnostic until those interactions have independent coverage.
|
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
|
||||||
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
||||||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||||
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
|
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,15 @@ Compare/status CPU regressions verify saved flags/masks, bank switching, sequent
|
||||||
Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping.
|
Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping.
|
||||||
They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors.
|
They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors.
|
||||||
ARM open-bus regressions cover unused-range boundaries, PC+8 source regions, byte lanes, signed/unaligned loads, writeback, and block loads.
|
ARM open-bus regressions cover unused-range boundaries, PC+8 source regions, byte lanes, signed/unaligned loads, writeback, and block loads.
|
||||||
They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/IWRAM-Thumb/DMA diagnostics.
|
They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/cold-IWRAM/DMA diagnostics.
|
||||||
Thumb open-bus tests cover supported code regions, both instruction alignments, ROM windows, mirrors, and physical memory wrap.
|
Thumb open-bus tests cover supported code regions, both instruction alignments, ROM windows, mirrors, and physical memory wrap.
|
||||||
They check widths, sign extension, rotation, block/stack/PC loads, aliases, modes, state preservation, and sequential/branch resampling.
|
They check widths, sign extension, rotation, block/stack/PC loads, aliases, modes, state preservation, and sequential/branch resampling.
|
||||||
Short-ROM tests require only the fetched halfword in 16-bit regions. IWRAM and region-crossing tests retain explicit diagnostics.
|
Short-ROM tests require only the fetched halfword in 16-bit regions. Cold IWRAM and region-crossing tests retain explicit diagnostics.
|
||||||
|
Sequential IWRAM regressions check retained lanes across fetches, reads, writes, mirrors, and physical RAM wrap.
|
||||||
|
They cover raw bus values before load rotation/sign extension, all modes, block transfers, and isolation from other memory regions.
|
||||||
|
Known-bit tests reject incomplete words. Transaction tests verify discarded partial updates and preserved history after errors.
|
||||||
|
Control-flow tests distinguish taken/untaken branches and invalidate PC writes even when the target equals fallthrough.
|
||||||
|
DMA and IRQ tests check conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs.
|
||||||
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
||||||
The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds.
|
The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds.
|
||||||
The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds.
|
The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds.
|
||||||
|
|
@ -228,7 +233,7 @@ They cover all processor modes, load widths, lanes, rotation, both Thumb code al
|
||||||
They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance.
|
They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance.
|
||||||
Thumb tests cover sequential snapshots, instruction-state transitions, SWI entry, and RAM-backed POP exits.
|
Thumb tests cover sequential snapshots, instruction-state transitions, SWI entry, and RAM-backed POP exits.
|
||||||
Unknown history, last valid fetch words, missing lookahead, and diagnostic-step retention have explicit checks.
|
Unknown history, last valid fetch words, missing lookahead, and diagnostic-step retention have explicit checks.
|
||||||
Thumb IWRAM unused-memory reads remain diagnostics. Separate tests cover supported general Thumb snapshots and retained BIOS history.
|
Unknown Thumb IWRAM history remains diagnostic. Separate tests cover known sequential history, general Thumb snapshots, and retained BIOS history.
|
||||||
Protected loads keep normal nominal costs and advance devices only through machine stepping.
|
Protected loads keep normal nominal costs and advance devices only through machine stepping.
|
||||||
Firmware readback regressions separately verify documented boot, SoftReset, returning SWI, IRQ callback, and IRQ return words.
|
Firmware readback regressions separately verify documented boot, SoftReset, returning SWI, IRQ callback, and IRQ return words.
|
||||||
They execute ARM/Thumb loads across widths and lanes, check caller state, and trace actual exits to their image data.
|
They execute ARM/Thumb loads across widths and lanes, check caller state, and trace actual exits to their image data.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue