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:
parent
8360ba4652
commit
46af481487
17 changed files with 761 additions and 28 deletions
130
crates/gba-core/src/memory/iwram_bus.rs
Normal file
130
crates/gba-core/src/memory/iwram_bus.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue