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

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