feat(memory): retain aligned Thumb BIOS prefetch words

Snapshot the full word at (executing PC+4)&~3 for Thumb BIOS execution, using strict image reads and incoming instruction state. Keep ARM PC+8 open bus separate and unchanged. Commit snapshots only after successful steps; preserve diagnostic behavior for unavailable lookahead.

Add original regression coverage for both code alignments, load widths and lanes, signed and unaligned values, processor modes, sequential fetches, ARM/Thumb transitions, SWI, RAM-backed POP exits, BIOS boundaries, failure retention, and nominal device progression. Record source evidence and explicit pipeline/hardware-validation limits.

Darwin arm64 validation: 878 workspace tests and 809 core/demo tests in each of debug and release; 39 Python tests; native ROM window tests in both profiles; ten window/graphics smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports match prior passing reports and match between profiles. No physical-hardware validation claimed.
This commit is contained in:
0xrsydn 2026-10-06 00:57:49 +07:00
commit cb4670c245
12 changed files with 427 additions and 25 deletions

View file

@ -7,6 +7,9 @@ use crate::{
timing::StepTiming,
};
#[path = "thumb_bios_access_tests.rs"]
mod thumb;
const ENTRY: u32 = 0x100;
const DATA: u32 = 0xf233_80a5;
const OTHER: u32 = 0x1234_5678;
@ -222,8 +225,8 @@ fn diagnostic_steps_keep_previous_bios_history_but_skipped_instructions_refresh_
}
#[test]
fn thumb_bios_execution_and_missing_lookahead_invalidate_unsupported_history() {
for (pc, thumb) in [(0x200, true), (0x3ff8, false), (0x3ffc, false)] {
fn missing_arm_or_thumb_lookahead_invalidates_unsupported_history() {
for (pc, thumb) in [(0x3ffc, true), (0x3ff8, false), (0x3ffc, false)] {
let mut bios = image();
word(&mut bios, pc, if thumb { 0x4720 } else { BX_R4 }); // Thumb BX r4 or ARM BX r4
let mut memory = bus(LDR, bios);

View file

@ -0,0 +1,287 @@
//! Thumb BIOS fetches drive a complete aligned word, unlike 16-bit ROM open bus.
use super::*;
const THUMB_ENTRY: u32 = 0x300;
const BX_R5: u16 = 0x4728;
fn half(bytes: &mut [u8], address: u32, value: u16) {
bytes[address as usize..address as usize + 2].copy_from_slice(&value.to_le_bytes());
}
fn thumb_image(pc: u32, instruction: u16) -> Vec<u8> {
let mut bios = image();
half(&mut bios, pc, instruction);
if pc < 0x3ffc {
word(&mut bios, (pc + 4) & !3, OTHER);
}
bios
}
fn enter_thumb(memory: &mut Memory, pc: u32, target_thumb: bool) -> Cpu {
let mut cpu = Cpu::new(ENTRY);
cpu.registers[4] = pc | 1;
cpu.registers[5] = ROM_START | u32::from(target_thumb);
cpu.step(memory).unwrap(); // Real ARM BX into Thumb BIOS, seed ARM history.
assert_eq!(cpu.pc(), pc);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
cpu
}
// Inspect via an independent CPU load without changing the BIOS CPU's state.
fn protected_word(memory: &mut Memory) -> u32 {
memory.write32(0x0200_0000, LDR).unwrap();
let mut probe = Cpu::new(0x0200_0000);
probe.step(memory).unwrap();
probe.registers[1]
}
#[test]
fn thumb_exit_retains_both_word_lanes_for_each_code_alignment_and_load_width() {
for offset in [0, 2] {
let pc = THUMB_ENTRY + offset;
for (arm, thumb, expected) in [
(
LDR,
0x6801,
[0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2],
),
(0xe5d0_1000, 0x7801, [0xa5, 0x80, 0x33, 0xf2]),
(
0xe1d0_10b0,
0x8801,
[0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2],
),
(
0xe1d0_10d0,
0x5681,
[0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2],
),
(
0xe1d0_10f0,
0x5e81,
[0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2],
),
] {
for target_thumb in [false, true] {
for address in [0, 0x100, 0x3ffc] {
for (lane, value) in expected.into_iter().enumerate() {
let mut bios = thumb_image(pc, BX_R5);
word(&mut bios, ENTRY + 8, OTHER); // Distinct incoming ARM history.
word(&mut bios, THUMB_ENTRY + 4, DATA);
// Poison PC+8, and PC+6's next word for the unaligned case.
word(&mut bios, THUMB_ENTRY + 8, 0xdead_beef);
let mut memory = bus(if target_thumb { thumb } else { arm }, bios);
let mut cpu = enter_thumb(&mut memory, pc, target_thumb);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
cpu.registers[0] = address + lane as u32;
cpu.flags.carry = true;
let mut after = cpu.clone();
after.registers[1] = value;
after.registers[15] += if target_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cpu, after, "pc={pc:#x} lane={lane} thumb={target_thumb}");
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);
}
}
}
}
}
}
#[test]
fn sequential_thumb_steps_sample_the_aligned_fetch_word_without_extra_cycles() {
let mut bios = thumb_image(THUMB_ENTRY, 0x46c0); // MOV r8,r8
half(&mut bios, THUMB_ENTRY + 2, 0x46c0);
half(&mut bios, THUMB_ENTRY + 4, BX_R5);
// At PC+4 and PC+6: the exit instruction plus a distinct high halfword.
let first = (OTHER & 0xffff_0000) | u32::from(BX_R5);
word(&mut bios, THUMB_ENTRY + 8, DATA);
let mut memory = bus(LDR, bios);
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, false);
assert_eq!(protected_word(&mut memory), DATA); // Incoming ARM prefetch.
for expected in [first, first, DATA] {
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(timing.data_cycles, 0);
assert_eq!(protected_word(&mut memory), expected);
assert_eq!(memory.cycles(), 0);
}
assert_eq!(cpu.pc(), ROM_START);
}
#[test]
fn thumb_to_arm_bios_transition_uses_incoming_state_then_refreshes_arm_history() {
for offset in [0, 2] {
let mut bios = thumb_image(THUMB_ENTRY + offset, BX_R5);
word(&mut bios, 0x208, DATA);
let mut memory = bus(LDR, bios);
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false);
cpu.registers[5] = 0x200; // Existing ARM BX r4, now with DATA at PC+8.
cpu.registers[4] = ROM_START;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(protected_word(&mut memory), OTHER);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
assert_eq!(protected_word(&mut memory), DATA);
}
}
#[test]
fn thumb_bios_protection_remains_pc_based_in_every_processor_mode() {
for mode in [
Mode::User,
Mode::System,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
] {
for offset in [0, 2] {
let pc = THUMB_ENTRY + offset;
let mut bios = thumb_image(pc, 0x6801); // LDR r1,[r0] inside BIOS.
half(&mut bios, pc + 2, BX_R5);
let mut memory = bus(LDR, bios);
let mut cpu = enter_thumb(&mut memory, pc, false);
cpu.apply_status(0xb000_00e0 | mode as u32, mode);
cpu.registers[0] = ENTRY + 8;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA); // Raw BIOS data, not retained OTHER.
assert_eq!(cpu.cpsr(), 0xb000_00e0 | mode as u32);
cpu.step(&mut memory).unwrap(); // BX to ARM ROM.
let expected = memory.read32((pc + 6) & !3).unwrap();
cpu.registers[0] = ENTRY + 8;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], expected);
assert_eq!(cpu.cpsr(), 0xb000_00c0 | mode as u32);
}
}
}
#[test]
fn thumb_pop_pc_retains_prefetch_not_stack_data_and_stays_thumb() {
for offset in [0, 2] {
let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY + offset, 0xbd00)); // POP {pc}
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, true);
cpu.registers[13] = 0x0300_0100;
memory.write32(0x0300_0100, ROM_START).unwrap();
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(timing.data_cycles, 1);
assert_eq!(cpu.registers[13], 0x0300_0104);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(cpu.pc(), ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
}
#[test]
fn thumb_swi_enters_arm_and_the_next_bios_instruction_replaces_thumb_history() {
for offset in [0, 2] {
let mut bios = thumb_image(THUMB_ENTRY + offset, 0xdf00); // SWI #0
word(&mut bios, 8, 0xe12f_ff15); // BX r5
word(&mut bios, 16, DATA);
let mut memory = bus(LDR, bios);
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), 8);
assert_eq!(cpu.mode(), Mode::Supervisor);
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(protected_word(&mut memory), OTHER);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.pc(), ROM_START);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
}
}
#[test]
fn last_complete_thumb_fetch_word_is_valid_at_both_alignments() {
for pc in [0x3ff8, 0x3ffa] {
let mut memory = bus(LDR, thumb_image(pc, BX_R5));
let mut cpu = enter_thumb(&mut memory, pc, false);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
for pc in [0x3ffc, 0x3ffe] {
let mut memory = bus(LDR, thumb_image(pc, BX_R5));
let mut cpu = enter_thumb(&mut memory, pc, false);
cpu.step(&mut memory).unwrap(); // Missing lookahead does not fail BX itself.
assert_eq!(cpu.pc(), ROM_START);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
for instruction in [0xde00, 0x6801] {
// Undefined instruction / failing LDR.
let mut memory = bus(LDR, thumb_image(THUMB_ENTRY, instruction));
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, false);
cpu.registers[0] = 0x0e00_0000;
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.cycles(), 0);
assert_eq!(memory.read32(THUMB_ENTRY + 4).unwrap(), OTHER); // Raw host read.
assert_eq!(protected_word(&mut memory), DATA); // Previous ARM history retained.
}
}
#[test]
fn outside_thumb_execution_keeps_history_but_does_not_enable_general_open_bus() {
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);
cpu.registers[0] = 0x4000;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0x4000)))
);
assert_eq!(cpu, before);
assert_eq!(memory.read32(0x4000), Err(MemoryError::Unmapped(0x4000)));
assert_eq!(protected_word(&mut memory), OTHER);
cpu.registers[0] = 0;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], OTHER);
}
#[test]
fn machine_steps_charge_thumb_exit_and_protected_load_without_snapshot_accesses() {
for offset in [0, 2] {
let mut memory = bus(LDR, thumb_image(THUMB_ENTRY + offset, BX_R5));
let cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false);
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
let cycles = machine.cycles();
assert_eq!(machine.last_timing().data_cycles, 0);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[1], OTHER);
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: 8,
data_cycles: 1,
internal_cycles: 1,
idle_cycles: 0,
}
);
assert_eq!(machine.cycles(), cycles + 10);
assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
machine.cycles()
);
}
}

View file

@ -57,12 +57,13 @@ fn vram_index(address: u32) -> usize {
struct CpuAccess {
pc: u32,
arm_prefetch: Option<u32>,
bios_prefetch: Option<u32>,
}
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
/// ARM data reads from unused memory return a PC+8 snapshot when available.
/// CPU data reads of BIOS from outside BIOS use its retained ARM PC+8 snapshot.
/// Thumb BIOS prefetch, general Thumb open bus, and DMA bus latches are not modeled.
/// Protected BIOS reads retain an ARM PC+8 or aligned Thumb PC+4 word snapshot.
/// General Thumb open bus, a full fetch pipeline, and DMA bus latches are not modeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -333,17 +334,31 @@ impl Memory {
// Sample before execution, without data timing or open-bus recursion.
// Missing lookahead bytes must not fail an instruction that never uses them.
let arm_prefetch = match instruction_set {
InstructionSet::Arm => self.snapshot_arm_prefetch(pc),
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
InstructionSet::Thumb => None,
};
self.cpu_access = Some(CpuAccess { pc, arm_prefetch });
let bios_prefetch = if pc < BIOS_SIZE as u32 {
match instruction_set {
InstructionSet::Arm => arm_prefetch,
// BIOS drives a full 32-bit word for a Thumb halfword fetch.
// Preserve both lanes of the aligned PC+4 word, not a repeated halfword.
InstructionSet::Thumb => self.snapshot_mapped_word((pc + 4) & !3),
}
} else {
None
};
self.cpu_access = Some(CpuAccess {
pc,
arm_prefetch,
bios_prefetch,
});
}
fn snapshot_arm_prefetch(&self, pc: u32) -> Option<u32> {
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
*byte = self
.read_mapped_byte(pc.wrapping_add(8 + offset as u32))
.read_mapped_byte(address.wrapping_add(offset as u32))
.ok()?;
}
Some(u32::from_le_bytes(bytes))
@ -353,8 +368,8 @@ impl Memory {
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.
// Thumb BIOS execution or missing lookahead invalidates the known value.
self.bios_prefetch = access.arm_prefetch;
// Missing lookahead invalidates history rather than exposing stale bytes.
self.bios_prefetch = access.bios_prefetch;
}
}
}