feat(memory): retain IWRAM lane effects across DMA

Update existing Thumb IWRAM continuation lanes from successful DMA reads and writes before the resumed PC+4 snapshot. Keep channel halfword duplication separate from actual lane accesses. Preserve atomic errors, cold-entry diagnostics, IRQ invalidation, and nominal timing.

Add original DMA matrices and resume regressions; update prior invalidation expectations. Record pinned ares and jgenesis evidence and explicit limits for cross-state history, general open bus, and sub-instruction arbitration.

Validated on Darwin arm64: 939 workspace and 870 core/demo tests per debug/release profile, 39 Python tests, native ROM windows, ten window smoke modes, fmt, clippy, and rustdoc. Public ARM/Thumb/memory/BIOS reports remain identical to the previous results.
This commit is contained in:
0xrsydn 2026-10-06 02:34:58 +07:00
commit 663b25e5d1
15 changed files with 449 additions and 45 deletions

View file

@ -22,6 +22,8 @@ mod exception_tests;
#[cfg(test)] #[cfg(test)]
mod instruction_tests; mod instruction_tests;
#[cfg(test)] #[cfg(test)]
mod iwram_dma_tests;
#[cfg(test)]
mod iwram_open_bus_tests; mod iwram_open_bus_tests;
#[cfg(test)] #[cfg(test)]
mod iwram_refill_tests; mod iwram_refill_tests;

View file

@ -0,0 +1,314 @@
//! Original instruction-boundary DMA/IWRAM history tests, not sub-instruction arbitration.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
};
const CODE: u32 = 0x0300_0100;
const IWRAM: u32 = 0x0300_1000;
const EWRAM: u32 = 0x0200_1000;
const SEED: u32 = 0x1234_abcd;
const DATA: u32 = 0x97e6_b580;
const NEXT: u16 = 0xd238;
const PROBE: u16 = 0x6806; // LDR r6,[r0]
const UNUSED: u32 = 0x8000_0000;
fn program(pc: u32, first: u16) -> (Cpu, Memory) {
let mut bus = Memory::new(vec![]).unwrap();
for (i, half) in [first, PROBE, 0x4321, NEXT].into_iter().enumerate() {
bus.write16(pc + i as u32 * 2, half).unwrap();
}
bus.write32(IWRAM, SEED).unwrap();
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu.registers[1] = IWRAM;
(cpu, bus)
}
fn ready(pc: u32) -> Machine {
let (cpu, bus) = program(pc, 0x680a); // LDR r2,[r1] establishes both lanes.
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
machine
}
fn configure(
bus: &mut Memory,
channel: usize,
source: u32,
destination: u32,
count: u16,
word: bool,
) {
let base = DMA_BASE + channel as u32 * DMA_STRIDE;
bus.write16(base + 10, 0).unwrap(); // Disable before rearming.
bus.write32(base, source).unwrap();
bus.write32(base + 4, destination).unwrap();
bus.write32(
base + 8,
(if word { 0x8400_0000 } else { 0x8000_0000 }) | u32::from(count),
)
.unwrap();
}
fn transfer(machine: &mut Machine, source: u32, destination: u32, word: bool) {
configure(machine.memory_mut(), 3, source, destination, 1, word);
let cpu = machine.cpu().clone();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.cpu(), &cpu);
}
// Independent byte-array reference. Only page 3 drives the local IWRAM lanes.
fn drive(lanes: &mut [u8; 4], address: u32, bytes: &[u8]) {
if address >> 24 == 3 {
let start = (address & 3) as usize;
lanes[start..start + bytes.len()].copy_from_slice(bytes);
}
}
fn resumed(lanes: [u8; 4], pc: u32) -> u32 {
let mut lanes = lanes;
drive(&mut lanes, pc + 6, &NEXT.to_le_bytes());
u32::from_le_bytes(lanes)
}
#[test]
fn dma_source_and_destination_drive_only_addressed_iwram_lanes_before_resume_fetch() {
for pc in [CODE, CODE + 2, CODE + 0x8000, 0x0300_7ffc, 0x0300_7ffe] {
for word in [false, true] {
for source in [IWRAM + 0x8000, IWRAM + 0x8002, EWRAM, EWRAM + 2] {
for destination in [IWRAM + 0x100, IWRAM + 0x102, EWRAM + 0x100, EWRAM + 0x102] {
let width = if word { 4 } else { 2 };
let source = source & !(width - 1);
let destination = destination & !(width - 1);
let mut machine = ready(pc);
machine.memory_mut().write32(source & !3, DATA).unwrap();
let bytes = DATA.to_le_bytes();
let lane = (source & 3) as usize;
let bytes = &bytes[lane..lane + width as usize];
let mut expected = SEED.to_le_bytes();
drive(&mut expected, source, bytes);
drive(&mut expected, destination, bytes);
transfer(&mut machine, source, destination, word);
// Host reads/writes after DMA cannot replace retained lanes.
machine.memory_mut().write32(source & !3, 0).unwrap();
machine.memory().read32(destination & !3).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[6], resumed(expected, pc));
}
}
}
}
}
#[test]
fn halfword_channel_duplication_is_not_a_full_iwram_bus_write() {
let mut machine = ready(CODE);
machine.memory_mut().write32(IWRAM, DATA).unwrap();
// High source lane only: low IWRAM bus lane must retain SEED's low half.
transfer(&mut machine, IWRAM + 2, EWRAM, false);
machine.step().unwrap();
assert_eq!(
machine.cpu().registers()[6],
(u32::from(NEXT) << 16) | (SEED & 0xffff)
);
}
#[test]
fn blocked_source_updates_only_the_destination_lane_using_its_selected_channel_half() {
for pc in [CODE, CODE + 2] {
for destination in [IWRAM + 0x100, IWRAM + 0x102, EWRAM + 0x100] {
let mut machine = ready(pc);
machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, EWRAM + 8, true); // Seed channel without touching IWRAM.
transfer(&mut machine, 2, destination, false);
let half = (DATA >> ((destination & 2) * 8)) as u16;
let mut expected = SEED.to_le_bytes();
drive(&mut expected, destination, &half.to_le_bytes());
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[6], resumed(expected, pc));
assert_eq!(machine.memory().read16(destination).unwrap(), half);
}
}
}
#[test]
fn completed_units_and_priority_changes_use_access_order_not_channel_number() {
let mut machine = ready(CODE);
machine.memory_mut().write32(EWRAM, DATA).unwrap();
machine
.memory_mut()
.write32(EWRAM + 4, 0x7612_8abc)
.unwrap();
machine
.memory_mut()
.write32(EWRAM + 8, 0xaabb_eeff)
.unwrap();
configure(machine.memory_mut(), 3, EWRAM, IWRAM, 2, true);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
configure(machine.memory_mut(), 0, EWRAM + 8, IWRAM + 8, 1, true);
for channel in [0, 3] {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel });
}
machine.step().unwrap();
assert_eq!(
machine.cpu().registers()[6],
resumed(0x7612_8abc_u32.to_le_bytes(), CODE)
);
}
#[test]
fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history() {
for fail_destination in [false, true] {
let mut machine = ready(CODE);
machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, IWRAM, true);
// A valid IWRAM source must not drive history if its destination is invalid.
machine.memory_mut().write32(IWRAM, 0x1122_3344).unwrap();
configure(
machine.memory_mut(),
3,
if fail_destination { IWRAM } else { 0x0e00_0000 },
if fail_destination { 0x0800_0000 } else { IWRAM },
1,
true,
);
let before = machine.cpu().clone();
let cycles = machine.cycles();
let timing = machine.last_timing();
for _ in 0..2 {
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing);
}
machine
.memory_mut()
.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0)
.unwrap();
machine.memory_mut().write16(CODE + 2, 0xde00).unwrap(); // Unsupported instruction.
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
machine.memory_mut().write16(CODE + 2, PROBE).unwrap();
machine.step().unwrap();
assert_eq!(
machine.cpu().registers()[6],
resumed(DATA.to_le_bytes(), CODE)
);
}
}
#[test]
fn partial_continuation_can_gain_known_lanes_but_cold_direct_entry_stays_unknown() {
let (mut cpu, mut bus) = program(CODE, 0x46c0); // NOP fills only the low lane.
cpu.step(&mut bus).unwrap();
bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
bus.step_dma().unwrap().unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], resumed(DATA.to_le_bytes(), CODE));
// No executed instruction or refill established an expected continuation PC.
let (mut cpu, mut bus) = program(CODE, PROBE);
bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
bus.step_dma().unwrap().unwrap();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
}
#[test]
fn irq_and_pc_discontinuity_do_not_reuse_dma_updated_continuations() {
for irq in [false, true] {
let mut machine = ready(CODE);
machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, IWRAM, true);
let mut cpu = machine.cpu().clone();
if irq {
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);
} else {
cpu.registers[15] = CODE + 0x100;
machine.memory_mut().write16(cpu.pc(), PROBE).unwrap();
}
assert_eq!(
cpu.step(machine.memory_mut()),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
}
}
#[test]
fn arm_and_other_thumb_regions_keep_their_fetch_snapshot_not_dma_data() {
for thumb in [false, true] {
for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] {
if thumb && pc >> 24 == 3 {
continue; // The bounded IWRAM continuation is tested separately.
}
let mut bus = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(pc);
cpu.registers[0] = UNUSED;
let expected = if thumb {
cpu.instruction_set = InstructionSet::Thumb;
bus.write16(pc, PROBE).unwrap();
bus.write32(pc + 4, 0x83e6_92ab).unwrap();
if pc >> 24 == 7 {
0x83e6_92ab
} else {
0x92ab_92ab
}
} else {
bus.write32(pc, 0xe590_6000).unwrap(); // LDR r6,[r0].
bus.write32(pc + 8, 0x83e6_92ab).unwrap();
0x83e6_92ab
};
bus.write32(EWRAM, DATA).unwrap();
let mut machine = Machine::new(cpu, bus);
transfer(&mut machine, EWRAM, IWRAM, true);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[6], expected);
}
}
}
#[test]
fn history_changes_add_no_timing_and_match_timed_and_untimed_cpu_results() {
let mut machine = ready(CODE);
machine
.memory_mut()
.write32(TIMER_BASE, 0x0080_0000)
.unwrap();
let start = machine.cycles();
machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, IWRAM, true);
assert_eq!(machine.last_timing().code_cycles, 0);
assert_eq!(machine.last_timing().data_cycles, 7);
assert_eq!(machine.last_timing().internal_cycles, 2);
machine.step().unwrap();
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() - start, 12);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 12);
let (mut cpu, mut bus) = program(CODE, 0x680a);
cpu.step(&mut bus).unwrap();
bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
bus.step_dma().unwrap().unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(&cpu, machine.cpu());
assert_eq!(bus.cycles(), 9); // Only explicit DMA stepping advances clocks here.
}

View file

@ -338,7 +338,7 @@ fn pc_discontinuities_and_non_iwram_execution_do_not_reuse_old_history() {
} }
#[test] #[test]
fn successful_dma_invalidates_history_but_failed_dma_preserves_it() { fn successful_dma_updates_history_and_failed_dma_preserves_it() {
for fail in [false, true] { for fail in [false, true] {
let (cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]); let (cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
bus.write32(TIMER_BASE, 0x0080_0000).unwrap(); bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
@ -364,8 +364,9 @@ fn successful_dma_invalidates_history_but_failed_dma_preserves_it() {
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
assert_eq!(machine.memory().read32(0x0200_0000).unwrap(), DATA); assert_eq!(machine.memory().read32(0x0200_0000).unwrap(), DATA);
let cycles = machine.cycles(); let cycles = machine.cycles();
assert!(machine.step().is_err()); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cycles(), cycles); assert_eq!(machine.cpu().registers()[6], 0x9bcd_80a5);
assert_eq!(machine.cycles(), cycles + 3);
} }
assert_eq!( assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()), u64::from(machine.memory().read16(TIMER_BASE).unwrap()),

View file

@ -3,7 +3,7 @@ use super::*;
use crate::{ use crate::{
dma::{DMA_BASE, DMA_STRIDE}, dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE}, io::{IE, IME, TIMER_BASE},
machine::{Machine, MachineError, StepKind}, machine::{Machine, StepKind},
memory::{BIOS_SIZE, ROM_START}, memory::{BIOS_SIZE, ROM_START},
}; };
@ -373,7 +373,7 @@ fn target_pair_refill_does_not_replace_the_separate_bios_retained_word() {
} }
#[test] #[test]
fn successful_dma_between_refill_and_target_execution_invalidates_refill_history() { fn successful_dma_between_refill_and_target_execution_updates_refill_history() {
let (cpu, mut bus) = branch(ROM_START, false, TARGET); let (cpu, mut bus) = branch(ROM_START, false, TARGET);
bus.write32(STACK, 0x1234_abcd).unwrap(); bus.write32(STACK, 0x1234_abcd).unwrap();
let mut machine = Machine::new(cpu, bus); let mut machine = Machine::new(cpu, bus);
@ -383,19 +383,12 @@ fn successful_dma_between_refill_and_target_execution_invalidates_refill_history
machine.memory_mut().write32(dma + 4, 0x0200_0000).unwrap(); machine.memory_mut().write32(dma + 4, 0x0200_0000).unwrap();
machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap(); machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 }); assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
let before = machine.cpu().clone();
let timing = machine.last_timing();
let cycles = machine.cycles(); let cycles = machine.cycles();
assert_eq!( assert_eq!(machine.step().unwrap(), StepKind::Instruction);
machine.step(), // DMA reads the whole IWRAM word, then the target+4 fetch replaces its low half.
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped( assert_eq!(machine.cpu().registers()[6], 0x1234_d234);
UNUSED assert_eq!(machine.cycles(), cycles + 3);
)))) assert_eq!(machine.cpu().pc(), TARGET + 2);
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.cpu().pc(), TARGET);
} }
#[test] #[test]

View file

@ -67,7 +67,8 @@ struct CpuAccess {
/// Protected BIOS reads separately retain the snapshot from BIOS execution. /// Protected BIOS reads separately retain the snapshot from BIOS execution.
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally. /// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally.
/// Successful refills into Thumb IWRAM sample the target pair to establish history. /// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// DMA channels retain transfer data separately; CPU bus handoff remains unmodeled. /// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// A full fetch pipeline and ARM-target refill history are not modeled. /// A full fetch pipeline and ARM-target refill history are not modeled.
pub struct Memory { pub struct Memory {
external_ram: Vec<u8>, external_ram: Vec<u8>,
@ -329,8 +330,13 @@ impl Memory {
AccessWidth::Byte => unreachable!("DMA is never byte-wide"), AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
} }
.map_err(map_error)?; .map_err(map_error)?;
// DMA-to-CPU bus ordering is not modeled. Never reuse pre-DMA IWRAM history. // DMA runs before the resumed CPU fetch. Only actual IWRAM accesses drive
self.invalidate_cpu_bus_history(); // its local lanes, in read/write order. Channel halfword duplication is not
// a full IWRAM bus write. Failed units never reach these history commits.
self.iwram_bus
.dma_access(transfer.source, transfer.width, data_latch);
self.iwram_bus
.dma_access(transfer.destination, transfer.width, value);
// Keep this channel active during its cycles: another edge must not // Keep this channel active during its cycles: another edge must not
// queue a second block while the current block is still transferring. // queue a second block while the current block is still transferring.
self.advance_cycles(timing.total()); self.advance_cycles(timing.total());

View file

@ -1,5 +1,6 @@
//! Transactional lane history for Thumb IWRAM execution and target-pair refills. //! Transactional lane history for Thumb IWRAM execution and target-pair refills.
//! Unknown startup/DMA history is not replaced with guessed memory bytes. //! Completed DMA accesses update existing continuation lanes before the next fetch.
//! Unknown startup history is not replaced with guessed memory bytes.
use std::cell::Cell; use std::cell::Cell;
use crate::timing::AccessWidth; use crate::timing::AccessWidth;
@ -76,6 +77,17 @@ impl IwramBus {
} }
} }
/// DMA runs between instructions. Commit only after the entire unit succeeds.
/// Keep the expected CPU continuation PC; do not invent one at cold startup.
pub(super) fn dma_access(&mut self, address: u32, width: AccessWidth, value: u32) {
debug_assert!(self.pending.get().is_none());
if address >> 24 == 3 {
if let Some(history) = &mut self.committed {
history.lanes.drive(address, width, value);
}
}
}
pub(super) fn finish(&mut self, succeeded: bool, sequential: bool) { pub(super) fn finish(&mut self, succeeded: bool, sequential: bool) {
let pending = self.pending.take(); let pending = self.pending.take();
if succeeded { if succeeded {

View file

@ -30,7 +30,8 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics | | `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics |
| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing | | `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing |
| `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing | | `crates/gba-core/src/cpu/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing |
| `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Sequential IWRAM lane history, access widths, isolation, errors, refills, IRQ/DMA invalidation, and nominal timing | | `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Sequential IWRAM lane history, access widths, isolation, errors, refills, IRQ invalidation, and nominal timing |
| `crates/gba-core/src/cpu/iwram_dma_tests.rs` | Bounded DMA continuation lanes, access ordering, channel isolation, diagnostics, and resume snapshots |
| `crates/gba-core/src/cpu/iwram_refill_tests.rs` | Thumb IWRAM target-pair refills, state changes, exception returns, boundaries, isolation, and nominal costs | | `crates/gba-core/src/cpu/iwram_refill_tests.rs` | Thumb IWRAM target-pair refills, state changes, exception returns, boundaries, isolation, and nominal costs |
| `crates/gba-core/src/memory/iwram_bus.rs` | Transactional known-lane history for Thumb IWRAM execution and target-pair refills | | `crates/gba-core/src/memory/iwram_bus.rs` | Transactional known-lane history for Thumb IWRAM execution and target-pair refills |
| `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions | | `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions |

View file

@ -128,7 +128,8 @@ See the [root-cause research](../research/bios-readback.md) for evidence and the
Host/debug reads and ROM-suite memory assertions still inspect raw mapped bytes and do not initialize or change the history. Host/debug reads and ROM-suite memory assertions still inspect raw mapped bytes and do not initialize or change the history.
Instruction fetches remain strict mapped reads outside the CPU data-access context. Instruction fetches remain strict mapped reads outside the CPU data-access context.
BIOS writes remain read-only diagnostics. DMA BIOS sources retain their existing unsupported-source diagnostic. BIOS writes remain read-only diagnostics. DMA BIOS sources never read image bytes.
They reuse known [channel data](dma.md#retained-channel-data), or report an unsupported-source diagnostic when channel data is unknown.
Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value. Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value.
The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance. The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance.

View file

@ -99,11 +99,12 @@ Limits remain explicit:
- Thumb open bus uses the supported region rules below, including bounded sequential internal-RAM lane history. - Thumb open bus uses the supported region rules below, including bounded sequential internal-RAM lane history.
- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots. - [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots.
- Unused/write-only I/O reads, DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here. - Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here.
[DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot. [DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot.
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics. - Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write. - Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write.
- There is no persistent instruction pipeline. Thumb IWRAM refills sample their target pair, but DMA-to-CPU transitions remain unmodeled. - There is no persistent instruction pipeline. Thumb IWRAM refills sample their target pair.
DMA updates existing IWRAM continuation lanes at instruction boundaries, not during CPU accesses.
This subset lets public ARM test 362 complete. That test does not assert the loaded data value. This subset lets public ARM test 362 complete. That test does not assert the loaded data value.
Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md). Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md).
@ -166,7 +167,8 @@ This preserves the existing atomic diagnostic policy without adding bus or devic
Taken branches and PC-writing refills end the old continuation history, even when their target is the fallthrough address. Taken branches and PC-writing refills end the old continuation history, even when their target is the fallthrough address.
A successful refill into Thumb IWRAM establishes new history from its target pair, as described below. A successful refill into Thumb IWRAM establishes new history from its target pair, as described below.
Untaken conditional branches keep sequential history. Untaken conditional branches keep sequential history.
Other execution states/regions, accepted machine IRQs, and successful DMA units invalidate continuation history. Other execution states/regions and accepted machine IRQs invalidate continuation history.
Successful DMA units update existing continuation lanes as described below.
A later exception return into Thumb IWRAM can establish new history through its refill. A later exception return into Thumb IWRAM can establish new history through its refill.
Failed DMA units leave history unchanged. Discontinuous entry without an executed refill starts unknown. Failed DMA units leave history unchanged. Discontinuous entry without an executed refill starts unknown.
@ -186,10 +188,32 @@ Snapshots use strict mapped reads and add no nominal data or device cycles.
They update bus history only; the interpreter does not cache these instructions for execution. They update bus history only; the interpreter does not cache these instructions for execution.
Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic. Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic.
Cold direct startup, unsupported target states/regions, and region-crossing lookahead still have no inferred refill history. Cold direct startup, unsupported target states/regions, and region-crossing lookahead still have no inferred refill history.
DMA between refill and arrival still invalidates this history rather than supplying a guessed DMA value. DMA between refill and arrival updates this continuation's actual IWRAM access lanes before the target+4 sample.
The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline. The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline.
ARM-target refill history, DMA ordering, self-modifying instruction execution, and exact refill timing remain incomplete. ARM-target refill history, sub-instruction DMA ordering, self-modifying instruction execution, and exact refill timing remain incomplete.
### DMA effects on Thumb IWRAM continuations
Within the instruction-boundary scheduler, DMA finishes before the next CPU PC+4 sample.
For an existing sequential or refill continuation, each successful DMA unit updates IWRAM source lanes, then IWRAM destination lanes.
Word accesses replace all lanes. Halfword accesses replace only the addressed halfword.
Other memory regions do not replace the local IWRAM value, even when DMA's channel data changes.
The resumed PC+4 fetch then replaces one halfword before the instruction's unused-memory snapshot is taken.
Channel halfword duplication is not a full-word IWRAM access.
A blocked source below work RAM does not drive IWRAM; an IWRAM destination still drives the halfword or word actually written.
Multiple units and channel preemption update lanes in access order, not channel-number order.
Host inspection/setup cannot change them.
History changes commit only after a unit succeeds. A failed unit retains the previous successful unit's history.
A failed resumed instruction discards its staged changes, not the completed DMA history.
DMA does not establish an expected PC at cold direct entry. Existing partially known continuations can gain known lanes.
PC discontinuities, unsupported state changes, IRQ entry, and region-crossing lookahead keep their existing conservative rules.
This implements local lane effects, not a general last-DMA-value override or a persistent pipeline.
Sub-instruction arbitration, DMA during CPU internal cycles, cold/cross-state IWRAM history, and exact resume timing remain incomplete.
See [the source evidence and limits](../research/iwram-bus-history.md#dma-continuation-extension).
## Processor status and exceptions ## Processor status and exceptions

View file

@ -55,8 +55,10 @@ DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifyi
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules. Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior. Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior.
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending. A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
Successful units invalidate bounded sequential Thumb IWRAM history because DMA-to-CPU latch ordering is not modeled. Successful units update existing Thumb IWRAM continuation lanes for their actual IWRAM reads and writes, in that order.
Failed units preserve that history. No DMA value is substituted for a CPU open-bus read. Accesses elsewhere leave those local lanes unchanged. Failed units preserve history.
The resumed CPU's PC+4 fetch then updates its addressed halfword. No universal DMA-value override is used.
See [bounded IWRAM DMA history](cpu.md#dma-effects-on-thumb-iwram-continuations) for the scope.
## Retained channel data ## Retained channel data
@ -83,5 +85,5 @@ Earlier successful units remain committed. Normal destination validation, nomina
This error policy is not a model of hardware data aborts. This error policy is not a model of hardware data aborts.
See [the latch evidence and scope](../research/dma-data-latches.md). See [the latch evidence and scope](../research/dma-data-latches.md).
General DMA open-bus reads, IWRAM lane effects, and CPU bus ownership on resume remain unimplemented. General DMA open-bus reads, cross-state IWRAM history, and sub-instruction bus ownership remain unimplemented.
In particular, the known channel value does not supply unused/write-only I/O reads or CPU open-bus snapshots. In particular, the known channel value does not supply unused/write-only I/O reads or CPU open-bus snapshots.

View file

@ -130,6 +130,15 @@ DMA data-latch research:
See [DMA data-latch findings](research/dma-data-latches.md). Source review is not a local hardware-test pass. See [DMA data-latch findings](research/dma-data-latches.md). Source review is not a local hardware-test pass.
DMA IWRAM continuation research:
- [jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs), for DMA read/write dispatch through the same lane-retaining IWRAM helpers as CPU accesses.
Its [DMA controller](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/dma.rs) and [memory storage](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/memory.rs) distinguish channel data from local bus lanes.
- The pinned ares DMA, bus dispatch, and IWRAM helpers above independently use the same local lane ownership.
See [the bounded DMA continuation extension](research/iwram-bus-history.md#dma-continuation-extension).
These source reviews do not establish sub-instruction arbitration, exact CPU resume timing, or hardware conformance.
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work. Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets. Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
The ignore file excludes `roms/`, common game ROM extensions, and save files. The ignore file excludes `roms/`, common game ROM extensions, and save files.

View file

@ -33,9 +33,10 @@ Its IWRAM bus and general data register are separate, and pending DMA runs befor
GBATEK's possible DMA influence on IWRAM is not sufficient to choose CPU resume ordering here. GBATEK's possible DMA influence on IWRAM is not sufficient to choose CPU resume ordering here.
Our instruction-boundary scheduler does not yet represent these per-access transitions. Our instruction-boundary scheduler does not yet represent these per-access transitions.
Therefore, this change keeps successful-DMA invalidation of bounded Thumb IWRAM history. The initial channel-latch change kept successful-DMA invalidation of bounded Thumb IWRAM history.
Failed DMA preserves that history. No channel value is injected into CPU or BIOS snapshots. The later [DMA continuation extension](iwram-bus-history.md#dma-continuation-extension) models local IWRAM lanes at instruction boundaries.
The next CPU-bus work needs independent resume-ordering coverage rather than a universal last-transfer override. Failed DMA preserves that history. No universal channel-value override is injected into CPU or BIOS snapshots.
General CPU-bus work still needs independent resume-ordering coverage rather than a universal last-transfer override.
## Implemented subset ## Implemented subset
@ -55,7 +56,8 @@ Other unmapped sources and unsupported controls remain diagnostics even with kno
The existing atomic diagnostic policy commits no new latch value for a failed unit. The existing atomic diagnostic policy commits no new latch value for a failed unit.
Earlier successful units remain visible. A partial failed read cannot replace their retained value. Earlier successful units remain visible. A partial failed read cannot replace their retained value.
Nominal source/destination costs, CPU pausing, trigger scheduling, and completion IRQs are unchanged. Nominal source/destination costs, CPU pausing, trigger scheduling, and completion IRQs are unchanged.
General DMA open-bus reads, write-only register readback, local IWRAM lane effects, and CPU resume ownership remain outside this subset. General DMA open-bus reads, write-only register readback, and general CPU resume ownership remain outside this subset.
Local IWRAM lane effects are now covered by the separately bounded continuation extension linked above.
## Validation scope ## Validation scope

View file

@ -52,8 +52,8 @@ The initial sequential-only implementation invalidated history after all refill
The extension below now establishes target-pair history for successful refills into Thumb IWRAM. The extension below now establishes target-pair history for successful refills into Thumb IWRAM.
The existing ARM/Thumb timing classifiers identify these refills for both timed and untimed CPU stepping. The existing ARM/Thumb timing classifiers identify these refills for both timed and untimed CPU stepping.
Execution outside Thumb IWRAM still ends a sequential history sequence. Execution outside Thumb IWRAM still ends a sequential history sequence.
An accepted machine IRQ and each successful DMA unit invalidate history. Failed DMA units preserve it. An accepted machine IRQ invalidates history. Failed DMA units preserve it.
The implementation does not guess DMA latch contents. The DMA extension below updates existing continuation lanes from successful IWRAM accesses.
This is not a full IWRAM bus model across all CPU states. ARM-target history and DMA-to-CPU ordering remain incomplete. This is not a full IWRAM bus model across all CPU states. ARM-target history and DMA-to-CPU ordering remain incomplete.
It is not a persistent instruction pipeline. Self-modifying instruction execution and exact per-access device timing remain unverified. It is not a persistent instruction pipeline. Self-modifying instruction execution and exact per-access device timing remain unverified.
@ -81,10 +81,41 @@ The interpreter still reads instructions when it executes them.
Changing T+2 after the branch does not replace its captured bus value; changing T+4 before arrival affects the later sample. Changing T+2 after the branch does not replace its captured bus value; changing T+4 before arrival affects the later sample.
Those original tests verify sample ordering, not hardware-accurate self-modifying instruction execution. Those original tests verify sample ordering, not hardware-accurate self-modifying instruction execution.
Successful DMA still invalidates history, including between refill and arrival. The refill-only implementation invalidated history on successful DMA, including between refill and arrival.
ARM-target refill history, exact pipeline timing, and DMA-to-CPU ordering remain outside this extension. The DMA extension below replaces that invalidation within existing continuations.
ARM-target refill history, exact pipeline timing, and sub-instruction DMA ordering remain incomplete.
No external emulator differential run or physical-hardware test was performed. No external emulator differential run or physical-hardware test was performed.
## DMA continuation extension
Two independent implementations route DMA through the same local IWRAM lane updates as CPU accesses:
- [ares bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) selects its IWRAM helpers for both CPU and DMA.
[Its DMA controller](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/dma.cpp) performs a source read before its destination write.
- [jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) routes `AccessCtx::DMA` through `read_iwram` and `write_iwram`.
Both call `iwram_update_open_bus`, which changes only addressed lanes and drives the general bus from the local word.
Its `try_progress_dma` performs source reads and destination writes in order.
Both run pending DMA before a CPU bus access. The access that resumes determines the subsequent bus value.
These source reviews support local lane effects, not a universal first-instruction DMA override.
They do not determine every hardware arbitration edge or validate our instruction-boundary scheduling.
Our bounded model applies those effects only when an existing Thumb IWRAM continuation has an expected next PC.
After a successful unit, the source and destination independently update their addressed IWRAM lanes.
DMA elsewhere leaves those lanes unchanged. A blocked source drives no IWRAM read lanes.
The destination sees the actual selected halfword, not an assumed duplicated full word.
The next PC+4 sample updates one halfword before the resumed instruction takes its snapshot.
This also applies between a completed refill and its first target instruction.
Only complete successful units commit history, preserving the project's atomic diagnostic policy.
Cold direct entry still has no expected continuation PC. DMA does not manufacture one, even after a word access.
An existing partially known continuation can gain lanes. IRQ entry and CPU discontinuities retain their invalidation rules.
General DMA open-bus reads, cross-state history, sub-instruction arbitration, and DMA during CPU internal cycles remain outside this subset.
The interpreter still fetches instructions directly; this extension does not fix self-modifying instruction-buffer behavior.
No physical-hardware measurements or external emulator differential runs were performed.
All regression programs are original, with a separate byte-array lane reference in the DMA matrix tests.
## Original regression coverage ## Original regression coverage
- Both code alignments, IWRAM mirrors, and physical RAM wrap. - Both code alignments, IWRAM mirrors, and physical RAM wrap.
@ -96,12 +127,15 @@ No external emulator differential run or physical-hardware test was performed.
- Taken and untaken branches, PC writes to fallthrough, discontinuities, IRQ entry, and DMA success/failure. - Taken and untaken branches, PC writes to fallthrough, discontinuities, IRQ entry, and DMA success/failure.
- ARM/Thumb source regions, both target alignments, refill sample ordering, stack/block returns, and saved User/System banks. - ARM/Thumb source regions, both target alignments, refill sample ordering, stack/block returns, and saved User/System banks.
- Actual SWI/IRQ return handlers, failed refills, unmapped targets, separate BIOS history, and DMA between refill and arrival. - Actual SWI/IRQ return handlers, failed refills, unmapped targets, separate BIOS history, and DMA between refill and arrival.
- DMA source/destination regions and alignments, word/halfword widths, mirrors, blocked-source lane selection, and other-region isolation.
- Channel preemption, completed-unit order, DMA/instruction failure retention, partial/cold continuations, and unchanged other CPU snapshot rules.
- Equal timed/untimed CPU results and unchanged nominal data/device costs. - Equal timed/untimed CPU results and unchanged nominal data/device costs.
## Validation result ## Validation result
The sequential-history regressions reproduced the old unsupported-read failures before implementation. The sequential-history regressions reproduced the old unsupported-read failures before implementation.
The refill regressions also failed on the prior invalidation-only path, then passed with target-pair sampling. The refill regressions also failed on the prior invalidation-only path, then passed with target-pair sampling.
The DMA continuation regressions reproduced unsupported-read failures under unconditional DMA invalidation, then passed with local lane updates.
Workspace and core/demo tests pass on Darwin arm64 in debug and release builds. Workspace and core/demo tests pass on Darwin arm64 in debug and release builds.
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes also pass. Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes also pass.
Public ARM, Thumb, memory, and BIOS reports match their previous passing results exactly. Public ARM, Thumb, memory, and BIOS reports match their previous passing results exactly.

View file

@ -137,8 +137,9 @@ ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot]
Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and bounded sequential IWRAM lane history. Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and bounded sequential IWRAM lane history.
Cold or invalidated IWRAM history remains diagnostic until all lanes are known. Cold or invalidated IWRAM history remains diagnostic until all lanes are known.
Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds. Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds.
Accepted machine IRQs, successful DMA, unsupported refills, and other execution states/regions invalidate continuation history. Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history.
This is not a fetch pipeline. ARM-target refill history, region-crossing fetches, DMA-to-CPU handoff, and unused/write-only I/O open bus remain unsupported. Successful DMA updates existing Thumb IWRAM continuation lanes for actual IWRAM accesses, before the resumed PC+4 sample.
This is not a fetch pipeline. ARM-target history, region-crossing fetches, general DMA handoff, and unused/write-only I/O open bus remain unsupported.
[DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history. [DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history.
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
Host inspection and instruction fetches remain strict. Other unmapped reads return errors. Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
@ -149,8 +150,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Extend the DMA channel-latch foundation with validated DMA-to-CPU bus ownership and IWRAM effects. 1. Extend bounded IWRAM DMA continuations toward general bus ownership with independent per-access resume tests.
General DMA open-bus reads need separate evidence; blocked-source reuse does not establish their values. General DMA open-bus reads and sub-instruction arbitration remain separate from retained channel data and local IWRAM lanes.
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage. Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history. Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images. The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.

View file

@ -75,8 +75,10 @@ Known-bit tests reject incomplete words. Transaction tests verify discarded part
Control-flow tests distinguish taken/untaken branches and refill Thumb IWRAM targets even when the target equals fallthrough. Control-flow tests distinguish taken/untaken branches and refill Thumb IWRAM targets even when the target equals fallthrough.
Refill tests cover ARM/Thumb BX, Thumb branches and BL, high-register PC writes, stack/block PC loads, and saved-state returns. Refill tests cover ARM/Thumb BX, Thumb branches and BL, high-register PC writes, stack/block PC loads, and saved-state returns.
They check both target alignments, mirrors/wrap, captured target+2 versus later target+4, widths, sign extension, and rotation. They check both target alignments, mirrors/wrap, captured target+2 versus later target+4, widths, sign extension, and rotation.
Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA invalidation, and BIOS isolation retain checks. Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA lane effects, and BIOS isolation retain checks.
DMA and IRQ tests check conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs. DMA tests compare source/destination lane updates against a byte-array reference, then apply the resumed PC+4 fetch.
They cover channel preemption, blocked sources, failures, mirrors, partial/cold continuations, and other-region snapshot isolation.
IRQ and PC-discontinuity tests retain conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs.
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks. Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds. The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds.
The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds. The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds.