diff --git a/crates/gba-core/src/cpu.rs b/crates/gba-core/src/cpu.rs index ca5cb4b..9b82f33 100644 --- a/crates/gba-core/src/cpu.rs +++ b/crates/gba-core/src/cpu.rs @@ -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; diff --git a/crates/gba-core/src/cpu/iwram_dma_tests.rs b/crates/gba-core/src/cpu/iwram_dma_tests.rs new file mode 100644 index 0000000..3c30045 --- /dev/null +++ b/crates/gba-core/src/cpu/iwram_dma_tests.rs @@ -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. +} diff --git a/crates/gba-core/src/cpu/iwram_open_bus_tests.rs b/crates/gba-core/src/cpu/iwram_open_bus_tests.rs index eb87b84..5063da8 100644 --- a/crates/gba-core/src/cpu/iwram_open_bus_tests.rs +++ b/crates/gba-core/src/cpu/iwram_open_bus_tests.rs @@ -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()), diff --git a/crates/gba-core/src/cpu/iwram_refill_tests.rs b/crates/gba-core/src/cpu/iwram_refill_tests.rs index 7ed5b81..e7d8bea 100644 --- a/crates/gba-core/src/cpu/iwram_refill_tests.rs +++ b/crates/gba-core/src/cpu/iwram_refill_tests.rs @@ -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] diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index cfa8a52..097dfec 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -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, @@ -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()); diff --git a/crates/gba-core/src/memory/iwram_bus.rs b/crates/gba-core/src/memory/iwram_bus.rs index d116f70..9f3cdcc 100644 --- a/crates/gba-core/src/memory/iwram_bus.rs +++ b/crates/gba-core/src/memory/iwram_bus.rs @@ -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 { diff --git a/docs/architecture.md b/docs/architecture.md index c8e23ff..8c8a337 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index 5159a77..b2e2b32 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -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. diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index 9e21e6f..5fbc08d 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -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 diff --git a/docs/hardware/dma.md b/docs/hardware/dma.md index c2803da..da6b3aa 100644 --- a/docs/hardware/dma.md +++ b/docs/hardware/dma.md @@ -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. diff --git a/docs/references.md b/docs/references.md index 6133899..fd96536 100644 --- a/docs/references.md +++ b/docs/references.md @@ -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. diff --git a/docs/research/dma-data-latches.md b/docs/research/dma-data-latches.md index 32d32f9..4a18cfa 100644 --- a/docs/research/dma-data-latches.md +++ b/docs/research/dma-data-latches.md @@ -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 diff --git a/docs/research/iwram-bus-history.md b/docs/research/iwram-bus-history.md index 4d603d0..ad8d178 100644 --- a/docs/research/iwram-bus-history.md +++ b/docs/research/iwram-bus-history.md @@ -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. diff --git a/docs/status.md b/docs/status.md index a724919..e56c291 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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. diff --git a/docs/testing.md b/docs/testing.md index db14ac0..cee07d5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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.