feat(cpu): charge non-refill code at pipeline fetch addresses

This commit is contained in:
0xrsydn 2026-10-06 15:34:45 +07:00
commit 0650b60908
26 changed files with 413 additions and 46 deletions

View file

@ -42,6 +42,8 @@ mod fetch_boundary_tests;
#[cfg(test)]
mod fetch_history_tests;
#[cfg(test)]
mod fetch_timing_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod iwram_dma_tests;

View file

@ -302,12 +302,12 @@ fn protected_reads_charge_one_data_access_before_device_progress() {
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: 8,
code_cycles: 6, // New ARM fetch is at ROM_START+8, not the page boundary.
data_cycles: 1,
internal_cycles: 1,
idle_cycles: 0
}
);
assert_eq!(machine.cycles(), 10);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 10);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 8);
}

View file

@ -92,7 +92,7 @@ fn compare_psr_has_sequential_timing_for_each_operand_form() {
.unwrap();
assert_eq!(
timing.code_cycles,
bus_cycles(0, ROM_START, AccessWidth::Word, AccessKind::Sequential)
bus_cycles(0, ROM_START + 8, AccessWidth::Word, AccessKind::Sequential)
);
assert_eq!(timing.internal_cycles, internal);
assert_eq!(timing.data_cycles, 0);

View file

@ -190,9 +190,9 @@ fn host_accesses_and_clock_advances_do_not_consume_resume_but_untimed_cpu_does()
#[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.
(PC, 0x0591_0000, 0), // LDREQ skipped: Z is clear.
(PC, 0xe581_0000, 6), // STR already has an N code access.
(ROM_START + 0x20000 - 8, 0xe1a0_0000, 0), // PC+8 fetch boundary already forces N.
] {
let (mut cpu, mut bus) = program(false, pc, instruction);
dma(&mut bus);

View file

@ -86,7 +86,18 @@ fn mapped_video_and_rom_crossings_use_fetch_region_lanes_for_all_load_widths() {
expected,
"boundary={boundary_address:#x} pc={pc:#x}"
);
assert_eq!(timing.code_cycles, 1);
assert_eq!(
timing.code_cycles,
if boundary_address == ROM_START {
if distance == 4 {
5
} else {
3
}
} else {
1
}
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);
@ -177,7 +188,7 @@ fn refill_straddling_ewram_and_iwram_keeps_the_captured_target_lane() {
}
#[test]
fn rom_page_and_wait_window_crossings_use_mapped_halfwords_without_changing_nominal_timing() {
fn rom_page_and_wait_window_crossings_use_mapped_halfwords_and_fetch_address_costs() {
let mut rom = vec![0; ROM_CAPACITY];
for offset in [0, 0x0100_0000] {
rom[offset..offset + 4].copy_from_slice(&DATA.to_le_bytes());
@ -213,7 +224,7 @@ fn rom_page_and_wait_window_crossings_use_mapped_halfwords_without_changing_nomi
timing.code_cycles,
bus_cycles(
memory.waitcnt(),
pc,
pc + 4,
AccessWidth::Halfword,
AccessKind::Sequential
)

View file

@ -0,0 +1,217 @@
//! Original fetch-address timing tests. Access kinds/refills remain nominal summaries.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
memory::{ROM_CAPACITY, ROM_START},
};
const ARM_NOP: u32 = 0xe1a0_0000;
const THUMB_NOP: u16 = 0x46c0;
const RAM: u32 = 0x0200_1000;
const DEST: u32 = 0x0300_1000;
fn program(thumb: bool, pc: u32, rom_len: usize) -> (Cpu, Memory) {
let pattern = if thumb { 0x46c0_46c0_u32 } else { ARM_NOP };
let mut memory = Memory::new(pattern.to_le_bytes().repeat(rom_len / 4)).unwrap();
if pc < ROM_START {
if thumb {
memory.write16(pc - 2, THUMB_NOP).unwrap();
memory.write16(pc, THUMB_NOP).unwrap();
} else {
memory.write32(pc - 4, ARM_NOP).unwrap();
memory.write32(pc, ARM_NOP).unwrap();
}
}
let mut cpu = Cpu::new(pc);
if thumb {
cpu.instruction_set = InstructionSet::Thumb;
}
(cpu, memory)
}
fn dma(memory: &mut Memory) {
let base = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(base, RAM).unwrap();
memory.write32(base + 4, DEST).unwrap();
memory.write32(base + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
}
#[test]
fn non_refill_fetches_use_the_new_region_for_cold_and_retained_pipelines() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for (boundary, expected) in [
(0x0300_0000, 1), // EWRAM -> IWRAM.
(0x0700_0000, 1), // VRAM -> OAM.
(ROM_START, if thumb { 5 } else { 8 }), // OAM -> ROM at forced-N boundary.
] {
for warm in [false, true] {
let pc = boundary - 2 * width;
let (mut timed, mut memory) = program(thumb, pc, 32);
let (mut plain, mut plain_memory) = program(thumb, pc, 32);
if warm {
timed.registers[15] -= width;
plain.registers[15] -= width;
timed.step(&mut memory).unwrap();
plain.step(&mut plain_memory).unwrap();
}
let cost = timed.step_timed(&mut memory).unwrap();
plain.step(&mut plain_memory).unwrap();
assert_eq!(
cost.code_cycles, expected,
"thumb={thumb} boundary={boundary:#x}"
);
assert_eq!(cost.data_cycles, 0);
assert_eq!(cost.internal_cycles, 0);
assert_eq!(timed, plain);
assert_eq!(memory.cycles(), 0);
}
}
}
}
#[test]
fn rom_page_n_cost_occurs_at_fetch_boundary_not_two_instructions_later() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for window in 0..3 {
for first in 0..4 {
for second in 0..2 {
for prefetch in [0, 0x4000] {
let boundary = ROM_START + window as u32 * 0x0200_0000 + 0x20000;
let (mut cpu, mut memory) = program(thumb, boundary - 2 * width, 0x20020);
let waitcnt = (first << [2, 5, 8][window])
| (second << [4, 7, 10][window])
| prefetch;
memory.write16(WAITCNT, waitcnt).unwrap();
let n = [5, 4, 3, 9][first as usize];
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
for expected in [
n + if thumb { 0 } else { s },
if thumb { s } else { 2 * s },
if thumb { s } else { 2 * s },
] {
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, expected);
}
}
}
}
}
}
}
#[test]
fn lookahead_crossing_a_rom_wait_window_uses_the_new_windows_settings() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for (boundary, window) in [(0x0a00_0000, 1), (0x0c00_0000, 2)] {
let (cpu, mut memory) = program(thumb, boundary - 2 * width, ROM_CAPACITY);
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.clone();
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 memory).unwrap().code_cycles,
n + if thumb { 0 } else { s }
);
}
}
}
}
}
#[test]
fn dma_resume_changes_the_actual_fetch_kind_without_an_extra_access() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for resumed in [false, true] {
// Lookahead lies inside ROM, not at the forced-N page boundary.
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 32);
if resumed {
dma(&mut memory);
}
let expected = match (thumb, resumed) {
(false, false) => 6,
(false, true) => 8,
(true, false) => 3,
(true, true) => 5,
};
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, expected);
assert_eq!(
cpu.step_timed(&mut memory).unwrap().code_cycles,
if thumb { 3 } else { 6 }
);
}
}
}
#[test]
fn a_boundary_waitcnt_store_uses_old_fetch_settings_and_the_next_fetch_uses_new_settings() {
let mut rom = ARM_NOP.to_le_bytes().repeat(ROM_CAPACITY / 4);
rom[ROM_CAPACITY - 4..].copy_from_slice(&0xe581_0000_u32.to_le_bytes()); // STR r0,[r1].
let mut memory = Memory::new(rom).unwrap();
let mut cpu = Cpu::new(0x09ff_fffc);
cpu.registers[0] = 0xc0; // WS1 N=3, S=2.
cpu.registers[1] = WAITCNT;
let cost = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cost.code_cycles, 10); // Old WS1 N=5, S=5 at lookahead 0x0a000004.
assert_eq!(cost.data_cycles, 1);
assert_eq!(memory.waitcnt(), 0xc0);
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 4);
}
#[test]
fn unavailable_lookahead_keeps_nominal_address_cost_and_fails_only_when_executed() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 0);
assert_eq!(
cpu.step_timed(&mut memory).unwrap().code_cycles,
if thumb { 3 } else { 6 }
);
let before = cpu.clone();
assert_eq!(
cpu.step_timed(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn refill_summaries_do_not_add_the_new_lookahead_cost_a_second_time() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 32);
if thumb {
memory.write16(cpu.pc(), 0x4708).unwrap();
} else {
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);
}
}
#[test]
fn machine_advances_boundary_fetch_cost_before_sampling_the_next_irq() {
let (cpu, mut memory) = program(false, ROM_START - 4, 32);
memory.write32(TIMER_BASE, 0x00c0_fffb).unwrap(); // Overflow after five cycles.
memory.write16(IE, 8).unwrap();
memory.write16(IME, 1).unwrap();
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.last_timing().code_cycles, 6);
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
}

View file

@ -104,7 +104,7 @@ fn load_aliases_keep_loaded_values_for_all_widths_offsets_and_index_modes() {
assert_eq!(bus.cycles(), 0);
assert_eq!(
timing.code_cycles,
bus_cycles(0, ROM_START, AccessWidth::Word, AccessKind::Sequential)
bus_cycles(0, ROM_START + 8, AccessWidth::Word, AccessKind::Sequential)
);
assert_eq!(
timing.data_cycles,
@ -245,7 +245,7 @@ fn alias_io_load_reads_before_device_progress_and_irq_delivery() {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], expected);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 10); // ROM boundary: 8 code + 1 I/O data + 1 internal
assert_eq!(machine.cycles(), 8); // PC+8 fetch: 6 code + 1 I/O data + 1 internal
assert_ne!(machine.memory().read32(TIMER_BASE).unwrap(), expected);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);

View file

@ -76,7 +76,7 @@ fn open_bus_loads_select_lanes_rotate_and_sign_extend_normally() {
assert_cpu_arch_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(timing.code_cycles, 8); // ROM boundary; no extra lookahead cost
assert_eq!(timing.code_cycles, 6); // PC+8 fetch is past the ROM boundary.
assert_eq!(bus.cycles(), 0);
}
}
@ -115,7 +115,7 @@ fn prefetch_snapshot_uses_the_executing_region_and_rom_window() {
assert_eq!(timing.data_cycles, 1);
assert_eq!(
timing.code_cycles,
bus_cycles(0, pc, AccessWidth::Word, AccessKind::Sequential)
bus_cycles(0, pc + 8, AccessWidth::Word, AccessKind::Sequential)
);
}
}
@ -277,8 +277,8 @@ fn machine_charges_one_data_access_and_dma_does_not_inherit_cpu_context() {
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers[1], DATA);
assert_eq!(machine.cycles(), 10); // 8 code + 1 data + 1 internal
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 10);
assert_eq!(machine.cycles(), 8); // 6 code + 1 data + 1 internal
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 8);
let before = machine.cpu().clone();
let timing = machine.last_timing();
// DMA3 exposes all 28 source address bits. Save memory is still unsupported.
@ -294,7 +294,7 @@ fn machine_charges_one_data_access_and_dma_does_not_inherit_cpu_context() {
}))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 10);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(RAM).unwrap(), 0);
}

View file

@ -276,13 +276,13 @@ fn machine_steps_charge_thumb_exit_and_protected_load_without_snapshot_accesses(
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: 8,
code_cycles: 6, // Returned ARM code fetches at ROM_START+8.
data_cycles: 1,
internal_cycles: 1,
idle_cycles: 0,
}
);
assert_eq!(machine.cycles(), cycles + 10);
assert_eq!(machine.cycles(), cycles + 8);
assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
machine.cycles()

View file

@ -84,7 +84,7 @@ fn every_supported_region_and_alignment_supplies_its_own_bus_width() {
assert_eq!(cpu.pc(), pc + 2);
assert_eq!(
timing.code_cycles,
bus_cycles(0, pc, AccessWidth::Halfword, AccessKind::Sequential)
bus_cycles(0, pc + 4, AccessWidth::Halfword, AccessKind::Sequential)
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
@ -425,11 +425,11 @@ fn machine_timing_and_dma_remain_separate_from_thumb_snapshot_reads() {
let mut machine = Machine::new(cpu(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[1], 0x80a5_80a5);
assert_eq!(machine.last_timing().code_cycles, 5); // ROM boundary: 1 + 4 wait cycles.
assert_eq!(machine.last_timing().code_cycles, 3); // PC+4 fetch: 1 + 2 wait cycles.
assert_eq!(machine.last_timing().data_cycles, 1);
assert_eq!(machine.last_timing().internal_cycles, 1);
assert_eq!(machine.cycles(), 7);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 7);
assert_eq!(machine.cycles(), 5);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 5);
let before = machine.cpu().clone();
let timing = machine.last_timing();
let dma = DMA_BASE + 3 * DMA_STRIDE;
@ -444,7 +444,7 @@ fn machine_timing_and_dma_remain_separate_from_thumb_snapshot_reads() {
}))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 7);
assert_eq!(machine.cycles(), 5);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(RAM).unwrap(), 0);
}

View file

@ -1,6 +1,6 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Data costs come from actual bus accesses. Bus-history samples are separate from timing calculations.
//! PC writes use target-region code costs, following GBATEK's cycle summary.
//! 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.
use super::{Cpu, CpuError, InstructionSet};
use crate::{
@ -56,7 +56,7 @@ impl Cpu {
InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction),
};
let pc = self.pc();
let fetch_address = fetched.lookahead.address();
let width = self.instruction_set.access_width();
// A WAITCNT store affects subsequent instructions, not this code access.
let waitcnt = memory.waitcnt();
@ -68,7 +68,7 @@ impl Cpu {
let code_cycles = if summary.refill {
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
} else {
bus_cycles(waitcnt, pc, width, code_kind)
bus_cycles(waitcnt, fetch_address, width, code_kind)
};
Ok(StepTiming {
code_cycles,

View file

@ -11,6 +11,12 @@ pub(crate) struct InstructionFetch {
pub(super) bus_word: Option<u32>,
}
impl InstructionFetch {
pub(crate) fn address(&self) -> u32 {
self.address
}
}
impl Memory {
pub(crate) fn fetch_instruction(
&self,

View file

@ -59,7 +59,7 @@ fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
}
}
assert_eq!(entries, 1);
assert_eq!(machine.cycles(), 153);
assert_eq!(machine.cycles(), 151); // Initial non-refill ROM fetch is S+S, not N+S.
assert_eq!(machine.cpu().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 0x2c);
assert_eq!(machine.cpu().registers()[10], 1);
@ -84,7 +84,7 @@ fn overflow_after_instruction_is_sampled_on_next_step() {
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
assert_eq!(machine.cycles(), 11); // First ROM access: 8; BIOS IRQ refill: 3
assert_eq!(machine.cycles(), 9); // New ROM fetch at PC+8: 6; nominal BIOS IRQ refill: 3
}
#[test]
@ -254,7 +254,7 @@ fn skipped_condition_charges_code_access_but_not_registers() {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 8);
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
}

View file

@ -128,7 +128,8 @@ fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
#[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
for (waitcnt, entry_step, instructions, cycles) in [(0, 4, 3, 23), (0x18, 6, 5, 24)] {
// 20-cycle timer: four 6-cycle fetches or five 4-cycle fetches, then 3-cycle IRQ entry.
for (waitcnt, entry_step, instructions, cycles) in [(0, 5, 4, 27), (0x18, 6, 5, 23)] {
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles