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:
0xrsydn 2026-10-06 01:43:37 +07:00
commit 46af481487
17 changed files with 761 additions and 28 deletions

View file

@ -22,6 +22,8 @@ mod exception_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod iwram_open_bus_tests;
#[cfg(test)]
mod load_alias_tests;
#[cfg(test)]
mod open_bus_tests;
@ -208,9 +210,15 @@ impl Cpu {
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
// Record the executing instruction, not an operand's pipelined PC value.
// 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);
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
}

View 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)))
);
}

View file

@ -233,7 +233,7 @@ fn missing_bios_rom_save_and_unsupported_io_reads_remain_errors() {
}
#[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 cpu = Cpu::new(ROM_START);
cpu.registers[0] = UNUSED;

View file

@ -238,12 +238,12 @@ fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
}
#[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 cpu = enter_thumb(&mut memory, THUMB_ENTRY, true);
cpu.step(&mut memory).unwrap();
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[0] = 0x4000;
let before = cpu.clone();

View file

@ -293,7 +293,7 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
}
#[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 offset in [0, 2] {
let pc = base + offset;

View file

@ -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 {
match instruction {
0x4000..=0x43ff => {

View file

@ -178,6 +178,7 @@ impl Machine {
return Ok(StepKind::HaltIdle);
}
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))
} else {
(

View file

@ -1,5 +1,8 @@
use std::{cell::Cell, error::Error, fmt};
mod iwram_bus;
use iwram_bus::IwramBus;
use crate::{
cpu::InstructionSet,
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.
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
/// 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 {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -76,6 +80,7 @@ pub struct Memory {
data_timing: Cell<Option<DataTiming>>,
cpu_access: Option<CpuAccess>,
bios_prefetch: Option<u32>,
iwram_bus: IwramBus,
scanline_capture: Option<video::capture::Capture>,
sprite_pipeline: pipeline::SpritePipeline,
}
@ -98,6 +103,7 @@ impl Memory {
data_timing: Cell::new(None),
cpu_access: None,
bios_prefetch: None,
iwram_bus: IwramBus::default(),
scanline_capture: None,
sprite_pipeline: pipeline::SpritePipeline::default(),
})
@ -306,6 +312,8 @@ impl Memory {
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
}
.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
// queue a second block while the current block is still transferring.
self.advance_cycles(timing.total());
@ -332,6 +340,11 @@ impl Memory {
debug_assert!(self.cpu_access.is_none());
// Sample before execution, without data timing or open-bus recursion.
// 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 {
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
@ -348,20 +361,24 @@ impl Memory {
match pc >> 24 {
// BIOS and OAM drive a full word even for a halfword instruction fetch.
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.
0x02 | 0x05 | 0x06 | 0x08..=0x0d => {
let half = u16::from_le_bytes([
self.read_mapped_byte(address).ok()?,
self.read_mapped_byte(address + 1).ok()?,
]);
Some(u32::from(half) * 0x0001_0001)
Some(u32::from(self.snapshot_mapped_halfword(address)?) * 0x0001_0001)
}
// IWRAM needs the previous bus value, including intervening data loads/DMA.
// Unsupported code regions must not manufacture an open-bus snapshot.
_ => 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> {
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
@ -372,7 +389,8 @@ impl Memory {
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 succeeded && access.pc < BIOS_SIZE as u32 {
// 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 {
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
self.cpu_access
@ -442,6 +464,8 @@ impl Memory {
pub fn read8(&self, address: u32) -> Result<u8, MemoryError> {
let value = self.read_byte(address)?;
self.record_access(address, AccessWidth::Byte);
self.iwram_bus
.access(address, AccessWidth::Byte, u32::from(value));
Ok(value)
}
@ -520,6 +544,21 @@ impl Memory {
_ => 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 & !3 == KEYINPUT {
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)?]);
self.record_access(address, AccessWidth::Halfword);
self.iwram_bus
.access(address, AccessWidth::Halfword, u32::from(value));
Ok(value)
}
@ -630,6 +671,7 @@ impl Memory {
self.read_byte(address + 3)?,
]);
self.record_access(address, AccessWidth::Word);
self.iwram_bus.access(address, AccessWidth::Word, value);
Ok(value)
}
}

View 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));
}
}

View file

@ -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/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/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/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 |

View file

@ -97,12 +97,12 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no
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.
- 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.
- 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.
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 |
| 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 |
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.
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.
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.
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.
### 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
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.

View file

@ -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.
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.
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.

View file

@ -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.
- [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.
- [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.
- [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).
@ -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.
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.
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.

View 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.

View file

@ -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.
- Raw reads inside BIOS and protected reads outside it.
- 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

View file

@ -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 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.
- 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.
- 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.
@ -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.
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.
This is not a fetch pipeline. Thumb IWRAM, region-crossing fetches, DMA, and unused/write-only I/O open bus remain unsupported.
Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and bounded sequential IWRAM lane history.
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).
Host inspection and instruction fetches remain strict. Other unmapped reads 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
1. Add validated prior-bus history for Thumb IWRAM open bus, including data-load overwrites and DMA transitions.
Keep unknown history diagnostic until those interactions have independent coverage.
1. Extend IWRAM history across validated refill and DMA-to-CPU transitions.
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.
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.

View file

@ -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.
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.
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.
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.
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.
@ -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.
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.
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.
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.