feat(timing): break CPU sequential access after DMA
Retain one pending non-sequential resume after successful DMA. Apply it to the next nominal non-refill code access and consume it only on successful CPU or IRQ progress. Preserve failed-step atomicity, host and idle isolation, existing refill costs, and WAITCNT ordering. Add 13 original regressions for ARM/Thumb widths, ROM wait settings, sources, preemption, retries, mixed APIs, idle states, and interrupt timing. Record pinned hardware-test source and emulator evidence without claiming physical-hardware or full-pipeline conformance. Validated on Darwin arm64: 952 workspace and 883 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 unchanged.
This commit is contained in:
parent
663b25e5d1
commit
16286e0cbd
11 changed files with 437 additions and 4 deletions
|
|
@ -18,6 +18,8 @@ mod bios_access_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod compare_psr_tests;
|
mod compare_psr_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod dma_resume_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod exception_tests;
|
mod exception_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod instruction_tests;
|
mod instruction_tests;
|
||||||
|
|
|
||||||
302
crates/gba-core/src/cpu/dma_resume_tests.rs
Normal file
302
crates/gba-core/src/cpu/dma_resume_tests.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
//! Original tests of the nominal, instruction-boundary DMA resume cost.
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
dma::{DMA_BASE, DMA_STRIDE},
|
||||||
|
input::Buttons,
|
||||||
|
io::{HALTCNT, IE, IME, KEYCNT, TIMER_BASE, WAITCNT},
|
||||||
|
machine::{Machine, StepKind},
|
||||||
|
memory::{BIOS_SIZE, ROM_START},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RAM: u32 = 0x0200_1000;
|
||||||
|
const DEST: u32 = 0x0300_1000;
|
||||||
|
const PC: u32 = ROM_START + 0x100;
|
||||||
|
|
||||||
|
fn program(thumb: bool, pc: u32, instruction: u32) -> (Cpu, Memory) {
|
||||||
|
let nop = if thumb { 0x46c0_46c0_u32 } else { 0xe1a0_0000 };
|
||||||
|
let offset = (pc & 0x01ff_ffff) as usize;
|
||||||
|
let mut rom = nop.to_le_bytes().repeat((offset + 32) / 4);
|
||||||
|
let width = if thumb { 2 } else { 4 };
|
||||||
|
rom[offset..offset + width].copy_from_slice(&instruction.to_le_bytes()[..width]);
|
||||||
|
let mut cpu = Cpu::new(pc);
|
||||||
|
if thumb {
|
||||||
|
cpu.instruction_set = InstructionSet::Thumb;
|
||||||
|
}
|
||||||
|
cpu.registers[1] = RAM;
|
||||||
|
(cpu, Memory::new(rom).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(bus: &mut Memory, channel: usize, source: u32, destination: u32, count: u16) {
|
||||||
|
let base = DMA_BASE + channel as u32 * DMA_STRIDE;
|
||||||
|
bus.write16(base + 10, 0).unwrap();
|
||||||
|
bus.write32(base, source).unwrap();
|
||||||
|
bus.write32(base + 4, destination).unwrap();
|
||||||
|
bus.write32(base + 8, 0x8400_0000 | u32::from(count))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dma(bus: &mut Memory) {
|
||||||
|
configure(bus, 3, RAM, DEST, 1);
|
||||||
|
assert_eq!(bus.step_dma().unwrap().unwrap().0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_post_dma_code_access_uses_n_then_s_for_all_rom_wait_settings() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
for window in 0..3 {
|
||||||
|
for first in 0..4 {
|
||||||
|
for second in 0..2 {
|
||||||
|
for prefetch in [0, 0x4000] {
|
||||||
|
let pc = PC + window as u32 * 0x0200_0000;
|
||||||
|
let instruction = if thumb { 0x46c0 } else { 0xe1a0_0000 };
|
||||||
|
let (mut cpu, mut bus) = program(thumb, pc, instruction);
|
||||||
|
let waitcnt = (first << [2, 5, 8][window])
|
||||||
|
| (second << [4, 7, 10][window])
|
||||||
|
| prefetch;
|
||||||
|
bus.write16(WAITCNT, waitcnt).unwrap();
|
||||||
|
dma(&mut bus);
|
||||||
|
// Independent table, not the production timing helper.
|
||||||
|
let n = [5, 4, 3, 9][first as usize];
|
||||||
|
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||||
|
n + if thumb { 0 } else { s }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||||
|
if thumb { s } else { 2 * s }
|
||||||
|
);
|
||||||
|
assert_eq!(bus.cycles(), 9); // CPU-only timed calls do not advance devices.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ram_and_rom_dma_sources_both_break_the_cpu_sequence() {
|
||||||
|
for source in [RAM, DEST, ROM_START] {
|
||||||
|
let (cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
configure(&mut bus, 3, source, DEST + 16, 1);
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 8);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_configuration_alone_is_not_a_dma_resume_but_blocked_source_completion_is() {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
configure(&mut bus, 3, RAM, DEST, 1);
|
||||||
|
// CPU-only execution does not service the configured transfer.
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
bus.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||||
|
configure(&mut bus, 3, 0, DEST, 1); // Known channel data, no BIOS read.
|
||||||
|
bus.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_units_and_channel_preemption_produce_one_resume_not_a_penalty_queue() {
|
||||||
|
let (cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
configure(&mut bus, 3, RAM, DEST, 2);
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
machine.step().unwrap();
|
||||||
|
configure(machine.memory_mut(), 0, RAM + 16, DEST + 16, 1);
|
||||||
|
for channel in [0, 3] {
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel });
|
||||||
|
}
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 8);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 6);
|
||||||
|
// Another block after CPU progress must mark the next resume again.
|
||||||
|
configure(machine.memory_mut(), 3, RAM, DEST, 1);
|
||||||
|
machine.step().unwrap();
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_dma_preserves_an_existing_resume_but_cannot_create_one() {
|
||||||
|
for completed in [false, true] {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
if completed {
|
||||||
|
dma(&mut bus);
|
||||||
|
}
|
||||||
|
configure(&mut bus, 3, 0x0e00_0000, DEST, 1);
|
||||||
|
let cycles = bus.cycles();
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert!(bus.step_dma().is_err());
|
||||||
|
assert_eq!(bus.cycles(), cycles);
|
||||||
|
}
|
||||||
|
bus.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||||
|
if completed { 8 } else { 6 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_cpu_fetch_and_data_access_preserve_resume_until_success() {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe591_0000); // LDR r0,[r1].
|
||||||
|
dma(&mut bus);
|
||||||
|
cpu.registers[15] = 0x0e00_0000;
|
||||||
|
assert!(cpu.step_timed(&mut bus).is_err());
|
||||||
|
bus.write32(DEST + 0x100, 0xffff_ffff).unwrap(); // Unsupported ARM encoding.
|
||||||
|
cpu.registers[15] = DEST + 0x100;
|
||||||
|
assert!(cpu.step_timed(&mut bus).is_err());
|
||||||
|
cpu.registers[15] = PC;
|
||||||
|
cpu.registers[1] = 0x0e00_0000;
|
||||||
|
let before = cpu.clone();
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert!(cpu.step_timed(&mut bus).is_err());
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
}
|
||||||
|
cpu.registers[1] = RAM;
|
||||||
|
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||||
|
assert_eq!(timing.code_cycles, 8);
|
||||||
|
assert_eq!(timing.data_cycles, 6);
|
||||||
|
assert_eq!(timing.internal_cycles, 1);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_accesses_and_clock_advances_do_not_consume_resume_but_untimed_cpu_does() {
|
||||||
|
for untimed in [false, true] {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
dma(&mut bus);
|
||||||
|
bus.read32(PC).unwrap();
|
||||||
|
bus.read32(RAM).unwrap();
|
||||||
|
bus.write32(DEST, 0).unwrap();
|
||||||
|
bus.advance_cycles(10);
|
||||||
|
if untimed {
|
||||||
|
cpu.step(&mut bus).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||||
|
if untimed { 6 } else { 8 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_charging() {
|
||||||
|
for (pc, instruction, data) in [
|
||||||
|
(PC, 0x0591_0000, 0), // LDREQ skipped: Z is clear.
|
||||||
|
(PC, 0xe581_0000, 6), // STR already has an N code access.
|
||||||
|
(ROM_START + 0x20000, 0xe1a0_0000, 0), // Boundary already forces N.
|
||||||
|
] {
|
||||||
|
let (mut cpu, mut bus) = program(false, pc, instruction);
|
||||||
|
dma(&mut bus);
|
||||||
|
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||||
|
assert_eq!(timing.code_cycles, 8);
|
||||||
|
assert_eq!(timing.data_cycles, data);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn waitcnt_cpu_store_uses_old_code_cost_but_dma_store_applies_before_resume() {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe581_0000);
|
||||||
|
cpu.registers[0] = 0x18;
|
||||||
|
cpu.registers[1] = WAITCNT;
|
||||||
|
dma(&mut bus);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 4);
|
||||||
|
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
bus.write32(RAM, 0x18).unwrap();
|
||||||
|
configure(&mut bus, 3, RAM, WAITCNT, 1);
|
||||||
|
bus.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 5); // N=3, S=2.
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ram_instruction_consumes_resume_instead_of_delaying_it_until_rom_execution() {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
bus.write32(DEST + 0x100, 0xe1a0_0000).unwrap();
|
||||||
|
cpu.registers[15] = DEST + 0x100;
|
||||||
|
dma(&mut bus);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 1);
|
||||||
|
cpu.registers[15] = PC;
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_refill_and_irq_entry_consume_resume_without_an_extra_nominal_access() {
|
||||||
|
let (mut cpu, mut bus) = program(false, PC, 0xea00_0000); // B PC+8.
|
||||||
|
dma(&mut bus);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 20); // Existing N+2S summary.
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
|
||||||
|
let (cpu, _) = program(false, PC, 0xe1a0_0000);
|
||||||
|
let mut bios = vec![0; BIOS_SIZE];
|
||||||
|
bios[0x18..0x1c].copy_from_slice(&0xe1a0_0000_u32.to_le_bytes());
|
||||||
|
let mut bus = Memory::with_bios(0xe1a0_0000_u32.to_le_bytes().repeat(80), bios).unwrap();
|
||||||
|
configure(&mut bus, 3, RAM, DEST, 1);
|
||||||
|
bus.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0xc400).unwrap();
|
||||||
|
bus.write16(IE, 1 << 11).unwrap();
|
||||||
|
bus.write16(IME, 1).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 3);
|
||||||
|
// Inspect the bus through a separate CPU after IRQ entry, before a handler runs.
|
||||||
|
let mut probe = Cpu::new(PC);
|
||||||
|
assert_eq!(
|
||||||
|
probe.step_timed(machine.memory_mut()).unwrap().code_cycles,
|
||||||
|
6
|
||||||
|
);
|
||||||
|
assert_eq!(machine.memory().cycles(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn halt_and_stop_idle_do_not_consume_a_completed_dma_resume() {
|
||||||
|
for stop in [false, true] {
|
||||||
|
let (cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
dma(&mut bus);
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
if stop {
|
||||||
|
machine.memory_mut().write16(KEYCNT, 0x4001).unwrap();
|
||||||
|
machine.memory_mut().write16(IE, 1 << 12).unwrap();
|
||||||
|
machine.memory_mut().write8(HALTCNT, 0x80).unwrap();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
|
||||||
|
machine.memory_mut().set_buttons(Buttons::from_bits(1));
|
||||||
|
} else {
|
||||||
|
machine.memory_mut().write16(IE, 8).unwrap();
|
||||||
|
machine
|
||||||
|
.memory_mut()
|
||||||
|
.write32(TIMER_BASE, 0x00c0_fff0)
|
||||||
|
.unwrap();
|
||||||
|
machine.memory_mut().write8(HALTCNT, 0).unwrap();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||||
|
}
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 8);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resumed_n_cost_advances_devices_and_defers_irq_to_the_following_step() {
|
||||||
|
let (cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
dma(&mut bus);
|
||||||
|
bus.write32(TIMER_BASE, 0x00c0_fff9).unwrap(); // Overflow after seven cycles.
|
||||||
|
bus.write16(IE, 8).unwrap();
|
||||||
|
bus.write16(IME, 1).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
let start = machine.cycles();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.cycles() - start, 8);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||||
|
}
|
||||||
|
|
@ -59,6 +59,7 @@ impl Cpu {
|
||||||
let width = self.instruction_set.access_width();
|
let width = self.instruction_set.access_width();
|
||||||
// A WAITCNT store affects subsequent instructions, not this code access.
|
// A WAITCNT store affects subsequent instructions, not this code access.
|
||||||
let waitcnt = memory.waitcnt();
|
let waitcnt = memory.waitcnt();
|
||||||
|
let code_kind = memory.cpu_code_kind(summary.code_kind);
|
||||||
memory.begin_data_timing();
|
memory.begin_data_timing();
|
||||||
let result = self.execute_fetched(instruction, memory);
|
let result = self.execute_fetched(instruction, memory);
|
||||||
let data_cycles = memory.end_data_timing();
|
let data_cycles = memory.end_data_timing();
|
||||||
|
|
@ -66,7 +67,7 @@ impl Cpu {
|
||||||
let code_cycles = if summary.refill {
|
let code_cycles = if summary.refill {
|
||||||
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
|
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
|
||||||
} else {
|
} else {
|
||||||
bus_cycles(waitcnt, pc, width, summary.code_kind)
|
bus_cycles(waitcnt, pc, width, code_kind)
|
||||||
};
|
};
|
||||||
Ok(StepTiming {
|
Ok(StepTiming {
|
||||||
code_cycles,
|
code_cycles,
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ pub struct Memory {
|
||||||
io: Io,
|
io: Io,
|
||||||
cycles: u64,
|
cycles: u64,
|
||||||
data_timing: Cell<Option<DataTiming>>,
|
data_timing: Cell<Option<DataTiming>>,
|
||||||
|
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
|
||||||
|
cpu_resume_nonsequential: bool,
|
||||||
cpu_access: Option<CpuAccess>,
|
cpu_access: Option<CpuAccess>,
|
||||||
bios_prefetch: Option<u32>,
|
bios_prefetch: Option<u32>,
|
||||||
iwram_bus: IwramBus,
|
iwram_bus: IwramBus,
|
||||||
|
|
@ -104,6 +106,7 @@ impl Memory {
|
||||||
io: Io::default(),
|
io: Io::default(),
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
data_timing: Cell::new(None),
|
data_timing: Cell::new(None),
|
||||||
|
cpu_resume_nonsequential: false,
|
||||||
cpu_access: None,
|
cpu_access: None,
|
||||||
bios_prefetch: None,
|
bios_prefetch: None,
|
||||||
iwram_bus: IwramBus::default(),
|
iwram_bus: IwramBus::default(),
|
||||||
|
|
@ -141,6 +144,8 @@ impl Memory {
|
||||||
/// Finish the instruction/IRQ entry that started while running. A STOP store
|
/// Finish the instruction/IRQ entry that started while running. A STOP store
|
||||||
/// still pays its full nominal cost before later steps freeze the system.
|
/// still pays its full nominal cost before later steps freeze the system.
|
||||||
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
|
pub(crate) fn complete_cpu_step(&mut self, cycles: u32) {
|
||||||
|
// IRQ entry consumes the resume too, without executing an instruction.
|
||||||
|
self.cpu_resume_nonsequential = false;
|
||||||
self.advance_running_cycles(cycles);
|
self.advance_running_cycles(cycles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,6 +346,7 @@ impl Memory {
|
||||||
// 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());
|
||||||
self.io.complete_dma_unit(channel, data_latch);
|
self.io.complete_dma_unit(channel, data_latch);
|
||||||
|
self.cpu_resume_nonsequential = true;
|
||||||
Ok(Some((channel, timing)))
|
Ok(Some((channel, timing)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,6 +419,10 @@ impl Memory {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) {
|
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) {
|
||||||
|
if succeeded {
|
||||||
|
// Timed and untimed CPU execution both consume a resume. Errors do not.
|
||||||
|
self.cpu_resume_nonsequential = false;
|
||||||
|
}
|
||||||
self.iwram_bus.finish(succeeded, sequential);
|
self.iwram_bus.finish(succeeded, sequential);
|
||||||
if let Some(access) = self.cpu_access.take() {
|
if let Some(access) = self.cpu_access.take() {
|
||||||
if succeeded && access.pc < BIOS_SIZE as u32 {
|
if succeeded && access.pc < BIOS_SIZE as u32 {
|
||||||
|
|
@ -486,6 +496,16 @@ impl Memory {
|
||||||
self.io.waitcnt()
|
self.io.waitcnt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the next nominal code access without consuming pending DMA history.
|
||||||
|
/// Successful instruction/IRQ completion consumes it; host reads and idle do not.
|
||||||
|
pub(crate) fn cpu_code_kind(&self, kind: AccessKind) -> AccessKind {
|
||||||
|
if self.cpu_resume_nonsequential {
|
||||||
|
AccessKind::NonSequential
|
||||||
|
} else {
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn begin_data_timing(&self) {
|
pub(crate) fn begin_data_timing(&self) {
|
||||||
debug_assert!(self.data_timing.get().is_none());
|
debug_assert!(self.data_timing.get().is_none());
|
||||||
self.data_timing.set(Some(DataTiming::default()));
|
self.data_timing.set(Some(DataTiming::default()));
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||||
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
||||||
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns |
|
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns |
|
||||||
| `crates/gba-core/src/cpu/timing.rs` | ARM/Thumb cycle summaries and data-access accounting |
|
| `crates/gba-core/src/cpu/timing.rs` | ARM/Thumb cycle summaries, nominal DMA resume, and data-access accounting |
|
||||||
|
| `crates/gba-core/src/cpu/dma_resume_tests.rs` | One-shot non-sequential DMA resume costs, wait settings, failures, idle, IRQs, and API isolation |
|
||||||
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
||||||
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||||
| `Cargo.toml` | Workspace members and shared package settings |
|
| `Cargo.toml` | Workspace members and shared package settings |
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,24 @@ The writable mask is `0x5fff`; the Game Pak type flag reads as GBA, and the uppe
|
||||||
A CPU write to WAITCNT does not retroactively change that instruction's code-access cost.
|
A CPU write to WAITCNT does not retroactively change that instruction's code-access cost.
|
||||||
The new settings apply to subsequent code accesses.
|
The new settings apply to subsequent code accesses.
|
||||||
|
|
||||||
|
### Nominal CPU resume after DMA
|
||||||
|
|
||||||
|
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential.
|
||||||
|
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
|
||||||
|
A non-refill instruction uses its normal width and current WAITCNT with N instead of S.
|
||||||
|
ARM word accesses retain an S cost for their second halfword.
|
||||||
|
Stores and ROM boundaries that already use N do not receive an additional cost.
|
||||||
|
|
||||||
|
Successful timed or untimed CPU execution consumes pending resume, including skipped conditions and internal-memory instructions.
|
||||||
|
Successful machine IRQ entry also consumes it. Failed CPU/DMA steps preserve it.
|
||||||
|
Host access, clock-only advancement, and HALT/STOP idle do not consume it.
|
||||||
|
A failed DMA unit cannot create pending resume.
|
||||||
|
DMA WAITCNT writes apply before resume; CPU WAITCNT writes still use the previous code-access settings.
|
||||||
|
|
||||||
|
Branch and exception refill summaries retain their existing destination-based `1N+2S` costs, without an added resume access.
|
||||||
|
The old-PC fetch is not modeled separately. This remains nominal timing, not a per-access pipeline.
|
||||||
|
See [the evidence and limitations](../research/dma-resume-timing.md).
|
||||||
|
|
||||||
Important timing limits:
|
Important timing limits:
|
||||||
|
|
||||||
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline.
|
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline.
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ CPU delivery waits until no DMA is ready. IRQ entry does not acknowledge IF.
|
||||||
Timing uses independent source and destination bus costs, including WAITCNT and 128 KiB ROM boundaries.
|
Timing uses independent source and destination bus costs, including WAITCNT and 128 KiB ROM boundaries.
|
||||||
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
||||||
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
||||||
Startup scheduling delays, channel-resumption costs, CPU fetch-state effects, and display-bus contention remain unmodeled.
|
A successful unit breaks the next nominal CPU code-access sequence, even if DMA accessed only RAM.
|
||||||
|
The resumed non-refill instruction uses N code timing once; later instructions return to their normal summaries.
|
||||||
|
See [CPU resume timing](cpu.md#nominal-cpu-resume-after-dma) for failure, idle, and refill rules.
|
||||||
|
Startup scheduling delays, channel-resumption costs, a full CPU fetch pipeline, and display-bus contention remain unmodeled.
|
||||||
|
|
||||||
Special timing modes (sound FIFO/video capture), Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
|
Special timing modes (sound FIFO/video capture), Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
|
||||||
DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes.
|
DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes.
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,14 @@ DMA IWRAM continuation research:
|
||||||
See [the bounded DMA continuation extension](research/iwram-bus-history.md#dma-continuation-extension).
|
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.
|
These source reviews do not establish sub-instruction arbitration, exact CPU resume timing, or hardware conformance.
|
||||||
|
|
||||||
|
DMA resume-timing research:
|
||||||
|
|
||||||
|
- [NanoBoyAdvance force-nseq-access test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/dma/force-nseq-access/source/main.c), for equal timer expectations after RAM-source and ROM-source DMA sequences.
|
||||||
|
- The pinned NanoBoyAdvance bus and ares DMA/bus implementations above explicitly break sequential CPU access after DMA ownership.
|
||||||
|
|
||||||
|
See [non-sequential CPU resume](research/dma-resume-timing.md) for the nominal subset and unmodeled refill/prefetch details.
|
||||||
|
The public test was reviewed, not executed. No hardware timing pass is claimed.
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
||||||
74
docs/research/dma-resume-timing.md
Normal file
74
docs/research/dma-resume-timing.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Non-sequential CPU resume after DMA
|
||||||
|
|
||||||
|
## Finding and evidence
|
||||||
|
|
||||||
|
DMA can break the CPU's sequential bus access even when the transfer uses only work RAM.
|
||||||
|
The prior instruction timing path always used its instruction summary, without retaining DMA ownership changes.
|
||||||
|
An ordinary ROM instruction after DMA therefore still received a sequential code-access cost.
|
||||||
|
|
||||||
|
Sources reviewed:
|
||||||
|
|
||||||
|
- [NanoBoyAdvance force-nseq-access test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/dma/force-nseq-access/source/main.c) compares EWRAM-source and ROM-source transfers during a short instruction sequence.
|
||||||
|
Both tests specify the same timer result. This is a whole-sequence expectation, not an isolated first-fetch measurement.
|
||||||
|
- [NanoBoyAdvance bus at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/bus.cc) forces non-sequential ROM access when the previous access was DMA and the current access is CPU.
|
||||||
|
The condition does not require the DMA source or destination to be ROM.
|
||||||
|
- [ares DMA at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/dma.cpp) marks ARM7 accesses non-sequential when DMA takes bus ownership.
|
||||||
|
Its [bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) uses that state for CPU burst selection.
|
||||||
|
|
||||||
|
These sources support a sequence break. They do not establish exact timing for our instruction-boundary scheduler.
|
||||||
|
The public test source was reviewed outside the repository. Its ROM was not imported, built, or executed here.
|
||||||
|
No physical-hardware measurement or external-emulator differential run was performed.
|
||||||
|
|
||||||
|
## Bounded implementation
|
||||||
|
|
||||||
|
A successful DMA unit sets a pending non-sequential CPU resume in `Memory`.
|
||||||
|
Further units and channel changes keep that single pending state. They do not queue additional costs.
|
||||||
|
A failed unit cannot create or consume it. A blocked-source unit that successfully writes known channel data sets it normally.
|
||||||
|
Register configuration, host reads/writes, and clock-only advancement do not consume it.
|
||||||
|
|
||||||
|
Before instruction execution, `Cpu::step_timed` captures the pending state and WAITCNT.
|
||||||
|
For a non-refill instruction, pending resume changes the nominal code-access kind to non-sequential.
|
||||||
|
The existing bus-cost function applies the code width and ROM-window wait settings.
|
||||||
|
An ARM word still uses a non-sequential first halfword and a sequential second halfword.
|
||||||
|
This is a change of access kind, not a fixed positive surcharge: some wait settings make N cheaper than S.
|
||||||
|
|
||||||
|
Successful CPU execution consumes the pending state in timed and untimed APIs.
|
||||||
|
Skipped conditions consume it too. Failed fetches, unsupported instructions, and failed data accesses preserve it for retry.
|
||||||
|
Execution in internal memory consumes it even when N and S costs are equal.
|
||||||
|
The state does not wait for a later ROM instruction.
|
||||||
|
|
||||||
|
Machine IRQ entry consumes it through successful CPU-step completion.
|
||||||
|
HALT/STOP idle does not consume it. Normal device progress, wake-up, and IRQ sampling rules still apply.
|
||||||
|
A CPU WAITCNT store uses the pre-instruction settings for its code access.
|
||||||
|
A completed DMA WAITCNT store changes the settings before CPU resume.
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
The model has no persistent fetch pipeline or per-access DMA arbitration.
|
||||||
|
It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch.
|
||||||
|
DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
|
||||||
|
|
||||||
|
Existing refill summaries remain destination-based `1N+2S` costs.
|
||||||
|
A resumed branch or exception consumes pending state without adding another nominal access.
|
||||||
|
This prevents double-counting the existing N component, but does not model the discarded old-PC fetch separately.
|
||||||
|
Stores and 128 KiB ROM boundaries already use N and receive no duplicate cost.
|
||||||
|
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
|
||||||
|
|
||||||
|
This change does not select a DMA value for CPU open-bus reads.
|
||||||
|
DMA channel data, bounded IWRAM lane history, and protected BIOS history remain separate.
|
||||||
|
General bus-data ownership and exact resume timing still need independent per-access validation.
|
||||||
|
|
||||||
|
## Original regression coverage
|
||||||
|
|
||||||
|
Tests cover ARM/Thumb widths, all three ROM windows, every N/S wait setting, and stored prefetch-bit values.
|
||||||
|
They check RAM/ROM DMA sources, repeated units, channel preemption, blocked-source completion, and configuration-only isolation.
|
||||||
|
Other checks cover CPU/DMA errors, mixed timed/untimed APIs, host isolation, WAITCNT writes, skipped conditions, stores, and ROM boundaries.
|
||||||
|
Refills, IRQ entry, internal-memory execution, HALT/STOP idle, timer advancement, and subsequent IRQ delivery have explicit checks.
|
||||||
|
The new resume cases failed on the prior sequential-only path before implementation.
|
||||||
|
|
||||||
|
## Validation result
|
||||||
|
|
||||||
|
Workspace and core/demo tests pass in debug and release on Darwin arm64.
|
||||||
|
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass.
|
||||||
|
Public ARM, Thumb, memory, and BIOS reports match their previous passing results exactly.
|
||||||
|
These suites are regression checks, not independent conformance tests for DMA resume timing.
|
||||||
|
|
@ -37,6 +37,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- Four direct memory access (DMA) channels, with immediate, VBlank, and visible-line HBlank triggers.
|
- Four direct memory access (DMA) channels, with immediate, VBlank, and visible-line HBlank triggers.
|
||||||
- Halfword/word DMA, address controls, repeat, channel priority, CPU pausing, and completion IRQs.
|
- Halfword/word DMA, address controls, repeat, channel priority, CPU pausing, and completion IRQs.
|
||||||
- Separate retained DMA channel data, with known-value reuse for blocked sources below work RAM.
|
- Separate retained DMA channel data, with known-value reuse for blocked sources below work RAM.
|
||||||
|
- One-shot non-sequential nominal CPU code timing after successful DMA, including RAM-only transfers.
|
||||||
- HALT with enabled-request wake-up, continued device clocks, and bounded idle steps.
|
- HALT with enabled-request wake-up, continued device clocks, and bounded idle steps.
|
||||||
- BIOS-only CPU writes to POSTFLG/HALTCNT, with HALT and keypad-wake STOP.
|
- BIOS-only CPU writes to POSTFLG/HALTCNT, with HALT and keypad-wake STOP.
|
||||||
- STOP freezes CPU/DMA/device clocks, retains device phases, and reports a stopped frame runner without spinning.
|
- STOP freezes CPU/DMA/device clocks, retains device phases, and reports a stopped frame runner without spinning.
|
||||||
|
|
@ -150,7 +151,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
1. Extend bounded IWRAM DMA continuations toward general bus ownership with independent per-access resume tests.
|
1. Extend bounded IWRAM DMA history and nominal non-sequential resume timing with independent per-access bus tests.
|
||||||
General DMA open-bus reads and sub-instruction arbitration remain separate from retained channel data and local IWRAM lanes.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,9 @@ Original channel-data tests cover word retention, halfword duplication, blocked-
|
||||||
They check cold/zero history, cancel/re-enable, source-counter crossings, repeats, priority, failed-unit rollback, and BIOS snapshot isolation.
|
They check cold/zero history, cancel/re-enable, source-counter crossings, repeats, priority, failed-unit rollback, and BIOS snapshot isolation.
|
||||||
These tests do not establish general DMA open-bus reads or DMA-to-CPU handoff. See [the research scope](research/dma-data-latches.md).
|
These tests do not establish general DMA open-bus reads or DMA-to-CPU handoff. See [the research scope](research/dma-data-latches.md).
|
||||||
Tests verify channel priority/preemption, CPU pausing, display/timer progress, IRQ acknowledgement/return, and WAITCNT costs.
|
Tests verify channel priority/preemption, CPU pausing, display/timer progress, IRQ acknowledgement/return, and WAITCNT costs.
|
||||||
|
CPU resume tests cover ARM/Thumb widths, all ROM-window wait settings, one-shot N timing, and RAM-only DMA.
|
||||||
|
They check errors, host isolation, mixed timed/untimed APIs, WAITCNT updates, refills, IRQ entry, HALT/STOP, and timer delivery.
|
||||||
|
These verify the [nominal resume subset](research/dma-resume-timing.md), not a full fetch pipeline or physical-hardware timing.
|
||||||
Diagnostic tests check failed-unit isolation, retained earlier transfers, unsupported modes, and CPU block-store atomicity.
|
Diagnostic tests check failed-unit isolation, retained earlier transfers, unsupported modes, and CPU block-store atomicity.
|
||||||
Graphics startup tests verify that DMA3, not CPU stores or host writes, fills every background pixel.
|
Graphics startup tests verify that DMA3, not CPU stores or host writes, fills every background pixel.
|
||||||
Display tests cover exact scanline boundaries, line-227 VBlank behavior, all 228 HBlank events, and read-only status fields.
|
Display tests cover exact scanline boundaries, line-227 VBlank behavior, all 228 HBlank events, and read-only status fields.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue