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