feat(cpu): account for source fetches in branch and IRQ refills
This commit is contained in:
parent
0650b60908
commit
2d7eb6e4eb
27 changed files with 638 additions and 87 deletions
|
|
@ -56,6 +56,8 @@ mod load_alias_tests;
|
|||
#[cfg(test)]
|
||||
mod open_bus_tests;
|
||||
#[cfg(test)]
|
||||
mod refill_timing_tests;
|
||||
#[cfg(test)]
|
||||
mod status_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ fn ram_instruction_consumes_resume_instead_of_delaying_it_until_rom_execution()
|
|||
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, 22); // Source N=8 + target N=8, S=6.
|
||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||
|
||||
let (cpu, _) = program(false, PC, 0xe1a0_0000);
|
||||
|
|
@ -249,14 +249,15 @@ fn branch_refill_and_irq_entry_consume_resume_without_an_extra_nominal_access()
|
|||
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);
|
||||
// Source N=8 + BIOS pair 2.
|
||||
assert_eq!(machine.last_timing().code_cycles, 10);
|
||||
// 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);
|
||||
assert_eq!(machine.memory().cycles(), 19); // DMA 9 + IRQ entry 10.
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Original fetch-address timing tests. Access kinds/refills remain nominal summaries.
|
||||
//! Original fetch-address timing tests. Source access kinds remain nominal summaries.
|
||||
use super::*;
|
||||
use crate::{
|
||||
dma::{DMA_BASE, DMA_STRIDE},
|
||||
|
|
@ -189,7 +189,7 @@ fn unavailable_lookahead_keeps_nominal_address_cost_and_fails_only_when_executed
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn refill_summaries_do_not_add_the_new_lookahead_cost_a_second_time() {
|
||||
fn refills_charge_one_source_fetch_and_two_target_fetches() {
|
||||
for thumb in [false, true] {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 32);
|
||||
|
|
@ -199,7 +199,10 @@ fn refill_summaries_do_not_add_the_new_lookahead_cost_a_second_time() {
|
|||
memory.write32(cpu.pc(), 0xe12f_ff11).unwrap();
|
||||
}
|
||||
cpu.registers[1] = DEST | u32::from(thumb);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 3);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 3 + 2 } else { 6 + 2 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ fn successful_dma_between_refill_and_target_execution_updates_refill_history() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() {
|
||||
fn refill_snapshots_keep_timed_untimed_results_and_charge_source_plus_target_pair() {
|
||||
for thumb in [false, true] {
|
||||
for destination in [TARGET, TARGET + 2] {
|
||||
let (cpu, mut bus) = branch(ROM_START, thumb, destination);
|
||||
|
|
@ -403,13 +403,22 @@ fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() {
|
|||
assert_eq!(machine.cpu(), &untimed);
|
||||
assert_eq!(
|
||||
machine.last_timing().code_cycles,
|
||||
if index == 0 { 3 } else { 1 }
|
||||
if index == 0 {
|
||||
if thumb {
|
||||
5
|
||||
} else {
|
||||
8
|
||||
}
|
||||
} else {
|
||||
1
|
||||
}
|
||||
);
|
||||
assert_eq!(machine.last_timing().data_cycles, index);
|
||||
assert_eq!(machine.last_timing().internal_cycles, index);
|
||||
}
|
||||
assert_eq!(machine.cycles(), 6);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6);
|
||||
let cycles = if thumb { 8 } else { 11 }; // Source fetch, target pair, then 1S+1N+1I load.
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), cycles as u16);
|
||||
assert_eq!(bus.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
279
crates/gba-core/src/cpu/refill_timing_tests.rs
Normal file
279
crates/gba-core/src/cpu/refill_timing_tests.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
//! Original tests with independent source-fetch and target-pair cost tables.
|
||||
use super::*;
|
||||
use crate::{
|
||||
dma::{DMA_BASE, DMA_STRIDE},
|
||||
io::{IE, IME, TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{BIOS_SIZE, ROM_START},
|
||||
timing::StepTiming,
|
||||
};
|
||||
|
||||
const RAM: u32 = 0x0300_0100;
|
||||
const ARM_NOP: u32 = 0xe1a0_0000;
|
||||
|
||||
fn bx(thumb: bool, pc: u32, target: u32, memory: &mut Memory) -> Cpu {
|
||||
let mut cpu = Cpu::new(pc);
|
||||
cpu.registers[0] = target;
|
||||
if thumb {
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
memory.write16(pc, 0x4700).unwrap();
|
||||
} else {
|
||||
memory.write32(pc, 0xe12f_ff10).unwrap();
|
||||
}
|
||||
cpu
|
||||
}
|
||||
|
||||
fn pending_irq(memory: &mut Memory) {
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
memory.write16(TIMER_BASE + 2, 0).unwrap();
|
||||
}
|
||||
|
||||
fn resume(memory: &mut Memory) {
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(dma, 0x0200_0000).unwrap();
|
||||
memory.write32(dma + 4, 0x0300_1000).unwrap();
|
||||
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bx_target_pairs_use_outgoing_width_and_charge_the_third_fetch_only_on_execution() {
|
||||
for source_thumb in [false, true] {
|
||||
for target_thumb in [false, true] {
|
||||
let width = if target_thumb { 2 } else { 4 };
|
||||
for window in 0..3 {
|
||||
let boundary = ROM_START + window as u32 * 0x0200_0000 + 0x20000;
|
||||
// Boundary in first, second, or third target slot, or no boundary.
|
||||
for slot in 0..4 {
|
||||
let target = boundary - slot * width;
|
||||
let pattern = if target_thumb { 0x46c0_46c0 } else { ARM_NOP };
|
||||
let mut memory =
|
||||
Memory::new(pattern.to_le_bytes().repeat(0x20020 / 4)).unwrap();
|
||||
let cpu = bx(
|
||||
source_thumb,
|
||||
RAM,
|
||||
target | u32::from(target_thumb),
|
||||
&mut memory,
|
||||
);
|
||||
for first in 0..4 {
|
||||
for second in 0..2 {
|
||||
for prefetch in [0, 0x4000] {
|
||||
memory
|
||||
.write16(
|
||||
WAITCNT,
|
||||
(first << [2, 5, 8][window])
|
||||
| (second << [4, 7, 10][window])
|
||||
| prefetch,
|
||||
)
|
||||
.unwrap();
|
||||
let n = [5, 4, 3, 9][first as usize];
|
||||
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||
let extra_half = if target_thumb { 0 } else { s };
|
||||
let mut cpu = cpu.clone();
|
||||
let target_pair =
|
||||
n + extra_half + if slot == 1 { n } else { s } + extra_half;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
StepTiming {
|
||||
code_cycles: 1 + target_pair,
|
||||
..StepTiming::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(cpu.pc(), target);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if slot == 2 {
|
||||
n + extra_half
|
||||
} else {
|
||||
s + extra_half
|
||||
}
|
||||
);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rom_source_fetch_uses_incoming_width_boundary_and_dma_override_before_ram_refill() {
|
||||
for thumb in [false, true] {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
for window in 0..3 {
|
||||
for boundary in [false, true] {
|
||||
for resumed in [false, true] {
|
||||
let pc = ROM_START
|
||||
+ window as u32 * 0x0200_0000
|
||||
+ if boundary { 0x20000 - 2 * width } else { 0x100 };
|
||||
let mut rom = vec![0; 0x20020];
|
||||
let instruction: u32 = if thumb { 0x4700 } else { 0xe12f_ff10 };
|
||||
let offset = (pc & 0x01ff_ffff) as usize;
|
||||
rom[offset..offset + width as usize]
|
||||
.copy_from_slice(&instruction.to_le_bytes()[..width as usize]);
|
||||
let mut memory = Memory::new(rom).unwrap();
|
||||
memory.write32(RAM, ARM_NOP).unwrap();
|
||||
memory.write32(RAM + 4, ARM_NOP).unwrap();
|
||||
for first in 0..4 {
|
||||
for second in 0..2 {
|
||||
memory
|
||||
.write16(
|
||||
WAITCNT,
|
||||
(first << [2, 5, 8][window]) | (second << [4, 7, 10][window]),
|
||||
)
|
||||
.unwrap();
|
||||
let mut cpu = Cpu::new(pc);
|
||||
if thumb {
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
}
|
||||
cpu.registers[0] = RAM;
|
||||
if resumed {
|
||||
resume(&mut memory);
|
||||
}
|
||||
let n = [5, 4, 3, 9][first as usize];
|
||||
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||
let source =
|
||||
if boundary || resumed { n } else { s } + if thumb { 0 } else { s };
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
source + 2
|
||||
);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 1);
|
||||
// Resume is consumed by the branch, not delayed until another ROM fetch.
|
||||
let mut probe = Cpu::new(pc);
|
||||
if thumb {
|
||||
probe.instruction_set = InstructionSet::Thumb;
|
||||
}
|
||||
probe.registers[0] = RAM;
|
||||
assert_eq!(
|
||||
probe.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if boundary { n } else { s } + if thumb { 0 } else { s } + 2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_charges_discarded_incoming_fetch_even_when_rom_is_missing() {
|
||||
for thumb in [false, true] {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
for (pc, normal, resumed) in [
|
||||
(
|
||||
0x0200_0100,
|
||||
if thumb { 3 } else { 6 },
|
||||
if thumb { 3 } else { 6 },
|
||||
),
|
||||
(RAM, 1, 1),
|
||||
(
|
||||
ROM_START - width,
|
||||
if thumb { 3 } else { 6 },
|
||||
if thumb { 5 } else { 8 },
|
||||
),
|
||||
(
|
||||
ROM_START + 0x100,
|
||||
if thumb { 3 } else { 6 },
|
||||
if thumb { 5 } else { 8 },
|
||||
),
|
||||
(
|
||||
ROM_START + 0x20000 - 2 * width,
|
||||
if thumb { 5 } else { 8 },
|
||||
if thumb { 5 } else { 8 },
|
||||
),
|
||||
(
|
||||
0x0c00_0000 - 2 * width,
|
||||
if thumb { 5 } else { 14 },
|
||||
if thumb { 5 } else { 14 },
|
||||
),
|
||||
] {
|
||||
for dma in [false, true] {
|
||||
let mut bios = vec![0; BIOS_SIZE];
|
||||
bios[0x18..0x1c].copy_from_slice(&ARM_NOP.to_le_bytes());
|
||||
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
||||
let mut cpu = Cpu::new(pc);
|
||||
if thumb {
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
}
|
||||
let cpsr = cpu.cpsr();
|
||||
if dma {
|
||||
resume(&mut memory);
|
||||
}
|
||||
pending_irq(&mut memory);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let start = machine.cycles();
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
let expected = if dma { resumed } else { normal } + 2;
|
||||
assert_eq!(
|
||||
machine.last_timing(),
|
||||
StepTiming {
|
||||
code_cycles: expected,
|
||||
..StepTiming::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(machine.cycles() - start, u64::from(expected));
|
||||
assert_eq!(machine.cpu().pc(), 0x18);
|
||||
assert_eq!(machine.cpu().registers()[14], pc + 4);
|
||||
assert_eq!(machine.cpu().spsr(), Some(cpsr));
|
||||
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Arm);
|
||||
// No old-PC opcode executes, even when its ROM bytes are missing.
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.last_timing().code_cycles, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_pair_crossing_a_wait_window_uses_each_fetch_region() {
|
||||
for thumb in [false, true] {
|
||||
let width = if thumb { 2 } else { 4 };
|
||||
for (boundary, pair) in [
|
||||
(0x0a00_0000, if thumb { 10 } else { 18 }),
|
||||
(0x0c00_0000, if thumb { 10 } else { 24 }),
|
||||
] {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
let mut cpu = bx(
|
||||
false,
|
||||
RAM,
|
||||
(boundary - width) | u32::from(thumb),
|
||||
&mut memory,
|
||||
);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 1 + pair);
|
||||
let before = cpu.clone();
|
||||
assert!(cpu.step_timed(&mut memory).is_err()); // Missing target deferred until current.
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masked_irq_does_not_sample_or_change_cpu_or_consume_dma_resume() {
|
||||
for cpu_mask in [false, true] {
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
pending_irq(&mut memory);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.irq_disabled = cpu_mask;
|
||||
if !cpu_mask {
|
||||
memory.write16(IME, 0).unwrap();
|
||||
}
|
||||
resume(&mut memory);
|
||||
let before = cpu.clone();
|
||||
let cycles = memory.cycles();
|
||||
assert_eq!(cpu.take_irq_timed(&mut memory), None);
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(memory.cycles(), cycles);
|
||||
assert_eq!(
|
||||
memory.cpu_code_kind(crate::timing::AccessKind::Sequential),
|
||||
crate::timing::AccessKind::NonSequential
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
||||
//! Non-refill code costs use the new fetch address; data costs use actual bus accesses.
|
||||
//! Access kinds and PC-write refill totals remain instruction-local nominal summaries.
|
||||
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
|
||||
//! Source access kinds remain instruction-local nominal summaries.
|
||||
|
||||
use super::{Cpu, CpuError, InstructionSet};
|
||||
use crate::{
|
||||
|
|
@ -65,11 +65,10 @@ impl Cpu {
|
|||
let result = self.execute_fetched(fetched, memory);
|
||||
let data_cycles = memory.end_data_timing();
|
||||
result?;
|
||||
let code_cycles = if summary.refill {
|
||||
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
|
||||
} else {
|
||||
bus_cycles(waitcnt, fetch_address, width, code_kind)
|
||||
};
|
||||
let mut code_cycles = bus_cycles(waitcnt, fetch_address, width, code_kind);
|
||||
if summary.refill {
|
||||
code_cycles += refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width());
|
||||
}
|
||||
Ok(StepTiming {
|
||||
code_cycles,
|
||||
data_cycles,
|
||||
|
|
@ -78,16 +77,24 @@ impl Cpu {
|
|||
})
|
||||
}
|
||||
|
||||
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
|
||||
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
|
||||
StepTiming {
|
||||
code_cycles: refill_cycles(
|
||||
memory.waitcnt(),
|
||||
self.pc(),
|
||||
self.instruction_set.access_width(),
|
||||
),
|
||||
..StepTiming::default()
|
||||
/// Accept IRQ between instructions, discard the old-state fetch, then refill ARM.
|
||||
/// A missing discarded fetch cannot prevent entry or become a current-slot error.
|
||||
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||
let state = self.instruction_set;
|
||||
let address = self.pc().wrapping_add(2 * state.width());
|
||||
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||
return None;
|
||||
}
|
||||
let waitcnt = memory.waitcnt();
|
||||
let kind = memory.cpu_code_kind(AccessKind::Sequential);
|
||||
let fetch = memory.fetch_instruction(address, state);
|
||||
memory.discarded_cpu_fetch(&fetch);
|
||||
self.refill_pipeline(memory);
|
||||
Some(StepTiming {
|
||||
code_cycles: bus_cycles(waitcnt, fetch.address(), state.access_width(), kind)
|
||||
+ refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width()),
|
||||
..StepTiming::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn arm_summary(&self, instruction: u32) -> Summary {
|
||||
|
|
@ -198,7 +205,7 @@ impl Cpu {
|
|||
Summary::branch()
|
||||
}
|
||||
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => Summary::branch(),
|
||||
// Thumb BL prefix is 1S in the old region; its suffix is 2S+1N at the target.
|
||||
// Thumb BL prefix fetches once; its suffix adds the target N+S pair.
|
||||
_ => Summary::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn cross_region_bx_uses_destination_width_and_wait_states() {
|
||||
fn cross_region_bx_uses_source_and_destination_widths_and_wait_states() {
|
||||
let (mut cpu, mut memory) = arm(0xe12f_ff10);
|
||||
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
|
||||
cpu.registers[0] = 0x0a00_0101; // IWRAM source 1 + Thumb WS1 target pair 5N + 5S.
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(11, 0, 0));
|
||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||
let (mut cpu, mut memory) = thumb(0x4700);
|
||||
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
|
||||
cpu.registers[0] = ROM_START + 0x100; // IWRAM source 1 + ARM WS0 target pair 8N + 6S.
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -130,7 +130,7 @@ fn load_pc_adds_refill_and_does_not_count_fetch_as_data() {
|
|||
let (mut cpu, mut memory) = arm(0xe590_f000);
|
||||
cpu.registers[0] = DATA;
|
||||
memory.write32(DATA, ROM_START + 0x100).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 6, 1));
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 6, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -321,7 +321,7 @@ fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
|
|||
cpu.registers[14] = ROM_START + 0x100;
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap(),
|
||||
expected(if thumb_state { 11 } else { 20 }, 0, 0)
|
||||
expected(if thumb_state { 9 } else { 15 }, 0, 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,9 +177,8 @@ impl Machine {
|
|||
self.last_timing = timing;
|
||||
return Ok(StepKind::HaltIdle);
|
||||
}
|
||||
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
|
||||
self.cpu.refill_pipeline(&mut self.memory);
|
||||
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
|
||||
let (kind, timing) = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
||||
(StepKind::IrqEntry, timing)
|
||||
} else {
|
||||
(
|
||||
StepKind::Instruction,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use std::{cell::Cell, error::Error, fmt};
|
|||
mod fetch;
|
||||
#[cfg(test)]
|
||||
mod fetch_tests;
|
||||
#[cfg(test)]
|
||||
mod irq_fetch_tests;
|
||||
mod iwram_bus;
|
||||
#[cfg(test)]
|
||||
mod iwram_history_tests;
|
||||
|
|
@ -432,6 +434,14 @@ impl Memory {
|
|||
}
|
||||
}
|
||||
|
||||
/// IRQ's discarded source fetch drives only its actual IWRAM lanes.
|
||||
/// Do not invent cold fills or commit a BIOS instruction snapshot: none executed.
|
||||
pub(crate) fn discarded_cpu_fetch(&mut self, fetch: &InstructionFetch) {
|
||||
self.iwram_bus.begin();
|
||||
self.drive_iwram_fetch(fetch);
|
||||
self.iwram_bus.finish(true);
|
||||
}
|
||||
|
||||
/// Successful IWRAM target fetches drive their lanes in order, in either CPU state.
|
||||
/// Other-region and unavailable target fetches leave the local latch unchanged.
|
||||
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
|
||||
|
|
|
|||
90
crates/gba-core/src/memory/irq_fetch_tests.rs
Normal file
90
crates/gba-core/src/memory/irq_fetch_tests.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
//! IRQ fetch history is distinct from executing a BIOS instruction.
|
||||
use super::*;
|
||||
use crate::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_STRIDE,
|
||||
io::{IE, IME, TIMER_BASE},
|
||||
machine::{Machine, StepKind},
|
||||
};
|
||||
|
||||
const RAM: u32 = 0x0300_0100;
|
||||
const SEED: u32 = 0xa1b2_c3d4;
|
||||
|
||||
fn seeded_irq(memory: &mut Memory) {
|
||||
// DMA supplies known lanes after CPU setup, before the IRQ's discarded fetch.
|
||||
memory.write32(0x0200_1000, SEED).unwrap();
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(dma, 0x0200_1000).unwrap();
|
||||
memory.write32(dma + 4, RAM + 0x100).unwrap();
|
||||
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
memory.write16(TIMER_BASE + 2, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_drives_only_the_discarded_iwram_fetch_in_incoming_state() {
|
||||
for thumb in [false, true] {
|
||||
for offset in [0, 2] {
|
||||
if !thumb && offset != 0 {
|
||||
continue;
|
||||
}
|
||||
let pc = RAM + offset;
|
||||
let mut memory = Memory::new(vec![]).unwrap();
|
||||
let cpu = if thumb {
|
||||
// Original EWRAM bootstrap: load Thumb target then BX.
|
||||
memory.write32(0x0200_0000, 0xe59f_0000).unwrap();
|
||||
memory.write32(0x0200_0004, 0xe12f_ff10).unwrap();
|
||||
memory.write32(0x0200_0008, pc | 1).unwrap();
|
||||
let mut cpu = Cpu::new(0x0200_0000);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu.step(&mut memory).unwrap();
|
||||
cpu
|
||||
} else {
|
||||
Cpu::new(pc) // Cold IRQ entry must not fill current/decode slots.
|
||||
};
|
||||
memory.write32(RAM, 0xffff_ffff).unwrap();
|
||||
memory.write32(RAM + 4, 0x5678_1234).unwrap();
|
||||
memory.write32(RAM + 8, 0x9abc_def0).unwrap();
|
||||
seeded_irq(&mut memory);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
let expected = if thumb {
|
||||
if offset == 0 {
|
||||
0xa1b2_1234
|
||||
} else {
|
||||
0x5678_c3d4
|
||||
}
|
||||
} else {
|
||||
0x9abc_def0
|
||||
};
|
||||
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(expected));
|
||||
// Missing BIOS vector fails next step, without reverting accepted IRQ history.
|
||||
let before = machine.cpu().clone();
|
||||
let cycles = machine.cycles();
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_fetches_do_not_commit_a_bios_instruction_snapshot_or_erase_local_history() {
|
||||
for pc in [0x100, ROM_START, 0x0e00_0000] {
|
||||
let mut memory = Memory::with_bios(vec![], vec![0; BIOS_SIZE]).unwrap();
|
||||
memory.bios_prefetch = Some(SEED);
|
||||
seeded_irq(&mut memory);
|
||||
let mut machine = Machine::new(Cpu::new(pc), memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
assert_eq!(machine.memory().bios_prefetch, Some(SEED));
|
||||
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(SEED));
|
||||
// BIOS retained history still follows the successful-instruction policy.
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().bios_prefetch, Some(0));
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,7 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
}
|
||||
}
|
||||
|
||||
/// Target/decode pair only. The caller separately charges the old-state source fetch.
|
||||
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
|
||||
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
|
||||
+ bus_cycles(
|
||||
|
|
@ -97,12 +98,6 @@ pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u3
|
|||
width,
|
||||
AccessKind::Sequential,
|
||||
)
|
||||
+ bus_cycles(
|
||||
waitcnt,
|
||||
target.wrapping_add(2 * width.bytes()),
|
||||
width,
|
||||
AccessKind::Sequential,
|
||||
)
|
||||
}
|
||||
|
||||
/// A data-only trace. Instruction fetches and branch refills are charged by the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue