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:
parent
e72c643640
commit
663b25e5d1
15 changed files with 449 additions and 45 deletions
|
|
@ -22,6 +22,8 @@ mod exception_tests;
|
|||
#[cfg(test)]
|
||||
mod instruction_tests;
|
||||
#[cfg(test)]
|
||||
mod iwram_dma_tests;
|
||||
#[cfg(test)]
|
||||
mod iwram_open_bus_tests;
|
||||
#[cfg(test)]
|
||||
mod iwram_refill_tests;
|
||||
|
|
|
|||
314
crates/gba-core/src/cpu/iwram_dma_tests.rs
Normal file
314
crates/gba-core/src/cpu/iwram_dma_tests.rs
Normal 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.
|
||||
}
|
||||
|
|
@ -338,7 +338,7 @@ fn pc_discontinuities_and_non_iwram_execution_do_not_reuse_old_history() {
|
|||
}
|
||||
|
||||
#[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] {
|
||||
let (cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
|
||||
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.memory().read32(0x0200_0000).unwrap(), DATA);
|
||||
let cycles = machine.cycles();
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.cpu().registers()[6], 0x9bcd_80a5);
|
||||
assert_eq!(machine.cycles(), cycles + 3);
|
||||
}
|
||||
assert_eq!(
|
||||
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use super::*;
|
|||
use crate::{
|
||||
dma::{DMA_BASE, DMA_STRIDE},
|
||||
io::{IE, IME, TIMER_BASE},
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{BIOS_SIZE, ROM_START},
|
||||
};
|
||||
|
||||
|
|
@ -373,7 +373,7 @@ fn target_pair_refill_does_not_replace_the_separate_bios_retained_word() {
|
|||
}
|
||||
|
||||
#[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);
|
||||
bus.write32(STACK, 0x1234_abcd).unwrap();
|
||||
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 + 8, 0x8400_0001).unwrap();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||
let before = machine.cpu().clone();
|
||||
let timing = machine.last_timing();
|
||||
let cycles = machine.cycles();
|
||||
assert_eq!(
|
||||
machine.step(),
|
||||
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
|
||||
UNUSED
|
||||
))))
|
||||
);
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.last_timing(), timing);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.cpu().pc(), TARGET);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
// DMA reads the whole IWRAM word, then the target+4 fetch replaces its low half.
|
||||
assert_eq!(machine.cpu().registers()[6], 0x1234_d234);
|
||||
assert_eq!(machine.cycles(), cycles + 3);
|
||||
assert_eq!(machine.cpu().pc(), TARGET + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ struct CpuAccess {
|
|||
/// Protected BIOS reads separately retain the snapshot from BIOS execution.
|
||||
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally.
|
||||
/// 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.
|
||||
pub struct Memory {
|
||||
external_ram: Vec<u8>,
|
||||
|
|
@ -329,8 +330,13 @@ 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();
|
||||
// DMA runs before the resumed CPU fetch. Only actual IWRAM accesses drive
|
||||
// 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
|
||||
// queue a second block while the current block is still transferring.
|
||||
self.advance_cycles(timing.total());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! 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 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) {
|
||||
let pending = self.pending.take();
|
||||
if succeeded {
|
||||
|
|
|
|||
|
|
@ -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/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/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/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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
|
||||
The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance.
|
||||
|
|
|
|||
|
|
@ -99,11 +99,12 @@ Limits remain explicit:
|
|||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
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.
|
||||
A successful refill into Thumb IWRAM establishes new history from its target pair, as described below.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 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.
|
||||
Successful units invalidate bounded sequential Thumb IWRAM history because DMA-to-CPU latch ordering is not modeled.
|
||||
Failed units preserve that history. No DMA value is substituted for a CPU open-bus read.
|
||||
Successful units update existing Thumb IWRAM continuation lanes for their actual IWRAM reads and writes, in that order.
|
||||
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
|
||||
|
||||
|
|
@ -83,5 +85,5 @@ Earlier successful units remain committed. Normal destination validation, nomina
|
|||
This error policy is not a model of hardware data aborts.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Our instruction-boundary scheduler does not yet represent these per-access transitions.
|
||||
|
||||
Therefore, this change keeps successful-DMA invalidation of bounded Thumb IWRAM history.
|
||||
Failed DMA preserves that history. No channel value is injected into CPU or BIOS snapshots.
|
||||
The next CPU-bus work needs independent resume-ordering coverage rather than a universal last-transfer override.
|
||||
The initial channel-latch change kept successful-DMA invalidation of bounded Thumb IWRAM history.
|
||||
The later [DMA continuation extension](iwram-bus-history.md#dma-continuation-extension) models local IWRAM lanes at instruction boundaries.
|
||||
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
|
||||
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 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.
|
||||
An accepted machine IRQ and each successful DMA unit invalidate history. Failed DMA units preserve it.
|
||||
The implementation does not guess DMA latch contents.
|
||||
An accepted machine IRQ invalidates history. Failed DMA units preserve it.
|
||||
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.
|
||||
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.
|
||||
Those original tests verify sample ordering, not hardware-accurate self-modifying instruction execution.
|
||||
|
||||
Successful DMA still invalidates history, including between refill and arrival.
|
||||
ARM-target refill history, exact pipeline timing, and DMA-to-CPU ordering remain outside this extension.
|
||||
The refill-only implementation invalidated history on successful DMA, including between refill and arrival.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Validation result
|
||||
|
||||
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 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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
Accepted machine IRQs, successful DMA, 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.
|
||||
Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -149,8 +150,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
|||
|
||||
## Next steps
|
||||
|
||||
1. Extend the DMA channel-latch foundation with validated DMA-to-CPU bus ownership and IWRAM effects.
|
||||
General DMA open-bus reads need separate evidence; blocked-source reuse does not establish their values.
|
||||
1. Extend bounded IWRAM DMA continuations toward general bus ownership with independent per-access resume tests.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA invalidation, and BIOS isolation retain checks.
|
||||
DMA and IRQ tests check conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs.
|
||||
Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA lane effects, and BIOS isolation retain checks.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue