feat(memory): add region-dependent Thumb open-bus snapshots

Repeat the mapped PC+4 halfword for Thumb code in EWRAM, palette RAM, VRAM, and ROM. Use the aligned PC+4 word for BIOS/OAM. Keep retained BIOS history separate from current unused-memory reads. Reuse one instruction snapshot without extra data cycles or host/fetch/DMA fallbacks.

Keep IWRAM and 16 MiB region-crossing fetches diagnostic until prior bus history is modeled. Add original regressions for regions, alignments, mirrors, physical wrap, signed and unaligned loads, block/stack/PC loads, aliases, modes, short ROMs, strict errors, BIOS isolation, and nominal device progression. Record documented rules and remaining limits.

Darwin arm64 validation: 890 workspace tests and 821 core/demo tests in each of debug and release; 39 preparation tests; native ROM windows in both profiles; ten window/graphics smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to previous passing results and between profiles. No physical-hardware or independent public Thumb open-bus conformance claim.
This commit is contained in:
0xrsydn 2026-10-06 01:25:41 +07:00
commit 8360ba4652
11 changed files with 528 additions and 36 deletions

View file

@ -56,14 +56,13 @@ fn vram_index(address: u32) -> usize {
#[derive(Clone, Copy)]
struct CpuAccess {
pc: u32,
arm_prefetch: Option<u32>,
bios_prefetch: Option<u32>,
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.
/// 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.
/// 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.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -333,25 +332,34 @@ 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 arm_prefetch = match instruction_set {
let prefetch = match instruction_set {
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
InstructionSet::Thumb => None,
InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
};
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),
self.cpu_access = Some(CpuAccess { pc, prefetch });
}
fn snapshot_thumb_prefetch(&self, pc: u32) -> Option<u32> {
let address = pc.wrapping_add(4);
// Region-crossing fetches need pipeline history, not a rule chosen from the old PC.
if address >> 24 != pc >> 24 {
return None;
}
match pc >> 24 {
// BIOS and OAM drive a full word even for a halfword instruction fetch.
0x00 | 0x07 => self.snapshot_mapped_word(address & !3),
// 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)
}
} else {
None
};
self.cpu_access = Some(CpuAccess {
pc,
arm_prefetch,
bios_prefetch,
});
// 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_word(&self, address: u32) -> Option<u32> {
@ -369,7 +377,7 @@ impl Memory {
if succeeded && access.pc < BIOS_SIZE as u32 {
// Commit only successful instruction snapshots under our diagnostic policy.
// Missing lookahead invalidates history rather than exposing stale bytes.
self.bios_prefetch = access.bios_prefetch;
self.bios_prefetch = access.prefetch;
}
}
}
@ -452,7 +460,7 @@ impl Memory {
.ok_or(MemoryError::Unmapped(address));
}
if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) {
if let Some(word) = self.cpu_access.and_then(|access| access.arm_prefetch) {
if let Some(word) = self.cpu_access.and_then(|access| access.prefetch) {
return Ok((word >> ((address & 3) * 8)) as u8);
}
}