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)] #[cfg(test)]
mod fetch_history_tests; mod fetch_history_tests;
#[cfg(test)] #[cfg(test)]
mod fetch_timing_tests;
#[cfg(test)]
mod instruction_tests; mod instruction_tests;
#[cfg(test)] #[cfg(test)]
mod iwram_dma_tests; mod iwram_dma_tests;

View file

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

View file

@ -192,7 +192,7 @@ fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_chargi
for (pc, instruction, data) in [ for (pc, instruction, data) in [
(PC, 0x0591_0000, 0), // LDREQ skipped: Z is clear. (PC, 0x0591_0000, 0), // LDREQ skipped: Z is clear.
(PC, 0xe581_0000, 6), // STR already has an N code access. (PC, 0xe581_0000, 6), // STR already has an N code access.
(ROM_START + 0x20000, 0xe1a0_0000, 0), // Boundary already forces N. (ROM_START + 0x20000 - 8, 0xe1a0_0000, 0), // PC+8 fetch boundary already forces N.
] { ] {
let (mut cpu, mut bus) = program(false, pc, instruction); let (mut cpu, mut bus) = program(false, pc, instruction);
dma(&mut bus); dma(&mut bus);

View file

@ -86,7 +86,18 @@ fn mapped_video_and_rom_crossings_use_fetch_region_lanes_for_all_load_widths() {
expected, expected,
"boundary={boundary_address:#x} pc={pc:#x}" "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.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1); assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0); assert_eq!(memory.cycles(), 0);
@ -177,7 +188,7 @@ fn refill_straddling_ewram_and_iwram_keeps_the_captured_target_lane() {
} }
#[test] #[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]; let mut rom = vec![0; ROM_CAPACITY];
for offset in [0, 0x0100_0000] { for offset in [0, 0x0100_0000] {
rom[offset..offset + 4].copy_from_slice(&DATA.to_le_bytes()); 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, timing.code_cycles,
bus_cycles( bus_cycles(
memory.waitcnt(), memory.waitcnt(),
pc, pc + 4,
AccessWidth::Halfword, AccessWidth::Halfword,
AccessKind::Sequential 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!(bus.cycles(), 0);
assert_eq!( assert_eq!(
timing.code_cycles, timing.code_cycles,
bus_cycles(0, ROM_START, AccessWidth::Word, AccessKind::Sequential) bus_cycles(0, ROM_START + 8, AccessWidth::Word, AccessKind::Sequential)
); );
assert_eq!( assert_eq!(
timing.data_cycles, 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.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], expected); assert_eq!(machine.cpu().registers()[0], expected);
assert_eq!(machine.cpu().pc(), ROM_START + 4); 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_ne!(machine.memory().read32(TIMER_BASE).unwrap(), expected);
assert_eq!(machine.memory().read16(IF).unwrap(), 8); assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); 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_cpu_arch_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
assert_eq!(timing.data_cycles, 1); assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_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); 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.data_cycles, 1);
assert_eq!( assert_eq!(
timing.code_cycles, 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); let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers[1], DATA); assert_eq!(machine.cpu().registers[1], DATA);
assert_eq!(machine.cycles(), 10); // 8 code + 1 data + 1 internal assert_eq!(machine.cycles(), 8); // 6 code + 1 data + 1 internal
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 10); assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 8);
let before = machine.cpu().clone(); let before = machine.cpu().clone();
let timing = machine.last_timing(); let timing = machine.last_timing();
// DMA3 exposes all 28 source address bits. Save memory is still unsupported. // 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.cpu(), &before);
assert_eq!(machine.cycles(), 10); assert_eq!(machine.cycles(), 8);
assert_eq!(machine.last_timing(), timing); assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(RAM).unwrap(), 0); 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!( assert_eq!(
machine.last_timing(), machine.last_timing(),
StepTiming { StepTiming {
code_cycles: 8, code_cycles: 6, // Returned ARM code fetches at ROM_START+8.
data_cycles: 1, data_cycles: 1,
internal_cycles: 1, internal_cycles: 1,
idle_cycles: 0, idle_cycles: 0,
} }
); );
assert_eq!(machine.cycles(), cycles + 10); assert_eq!(machine.cycles(), cycles + 8);
assert_eq!( assert_eq!(
u64::from(machine.memory().read16(TIMER_BASE).unwrap()), u64::from(machine.memory().read16(TIMER_BASE).unwrap()),
machine.cycles() 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!(cpu.pc(), pc + 2);
assert_eq!( assert_eq!(
timing.code_cycles, 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.data_cycles, 1);
assert_eq!(timing.internal_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); let mut machine = Machine::new(cpu(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[1], 0x80a5_80a5); 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().data_cycles, 1);
assert_eq!(machine.last_timing().internal_cycles, 1); assert_eq!(machine.last_timing().internal_cycles, 1);
assert_eq!(machine.cycles(), 7); assert_eq!(machine.cycles(), 5);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 7); assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 5);
let before = machine.cpu().clone(); let before = machine.cpu().clone();
let timing = machine.last_timing(); let timing = machine.last_timing();
let dma = DMA_BASE + 3 * DMA_STRIDE; 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.cpu(), &before);
assert_eq!(machine.cycles(), 7); assert_eq!(machine.cycles(), 5);
assert_eq!(machine.last_timing(), timing); assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(RAM).unwrap(), 0); assert_eq!(machine.memory().read32(RAM).unwrap(), 0);
} }

View file

@ -1,6 +1,6 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics. //! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Data costs come from actual bus accesses. Bus-history samples are separate from timing calculations. //! Non-refill code costs use the new fetch address; data costs use actual bus accesses.
//! PC writes use target-region code costs, following GBATEK's cycle summary. //! Access kinds and PC-write refill totals remain instruction-local nominal summaries.
use super::{Cpu, CpuError, InstructionSet}; use super::{Cpu, CpuError, InstructionSet};
use crate::{ use crate::{
@ -56,7 +56,7 @@ impl Cpu {
InstructionSet::Arm => self.arm_summary(instruction), InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_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(); 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();
@ -68,7 +68,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, code_kind) bus_cycles(waitcnt, fetch_address, width, code_kind)
}; };
Ok(StepTiming { Ok(StepTiming {
code_cycles, code_cycles,

View file

@ -11,6 +11,12 @@ pub(crate) struct InstructionFetch {
pub(super) bus_word: Option<u32>, pub(super) bus_word: Option<u32>,
} }
impl InstructionFetch {
pub(crate) fn address(&self) -> u32 {
self.address
}
}
impl Memory { impl Memory {
pub(crate) fn fetch_instruction( pub(crate) fn fetch_instruction(
&self, &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!(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().mode(), Mode::System);
assert_eq!(machine.cpu().pc(), ROM_START + 0x2c); assert_eq!(machine.cpu().pc(), ROM_START + 0x2c);
assert_eq!(machine.cpu().registers()[10], 1); 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.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8); 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] #[test]
@ -254,7 +254,7 @@ fn skipped_condition_charges_code_access_but_not_registers() {
assert_eq!(machine.step().unwrap(), StepKind::Instruction); assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], 0); assert_eq!(machine.cpu().registers()[0], 0);
assert_eq!(machine.cpu().pc(), ROM_START + 4); 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); 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] #[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() { 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(); let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap(); memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles

View file

@ -55,7 +55,8 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, deferred fetch diagnostics, refill sampling, and explicit invalidation | | `crates/gba-core/src/cpu/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, deferred fetch diagnostics, refill sampling, and explicit invalidation |
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence | | `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence | | `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence |
| `crates/gba-core/src/cpu/timing.rs` | ARM/Thumb cycle summaries, nominal DMA resume, and data-access accounting | | `crates/gba-core/src/cpu/timing.rs` | Fetch-address non-refill costs, nominal refill/DMA summaries, and data-access accounting |
| `crates/gba-core/src/cpu/fetch_timing_tests.rs` | Fetch-region/page/wait-window costs, DMA resume, WAITCNT stores, deferred errors, and timer/IRQ effects |
| `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/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 |

View file

@ -323,6 +323,12 @@ Thumb `BL` charges 1S for its prefix and 1N+2S for its suffix.
Multiply costs vary with the incoming multiplier's upper bytes; accumulate and long forms add internal cycles. Multiply costs vary with the incoming multiplier's upper bytes; accumulate and long forms add internal cycles.
ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register. ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register.
Non-refill code costs use the new instruction fetch address: P+8 for ARM and P+4 for Thumb.
The incoming instruction state selects the width. The sampled fetch address selects the memory region and ROM wait-state window.
A 128 KiB ROM boundary therefore forces N timing when lookahead reaches it, not when execution reaches it later.
This applies to cold and retained pipelines without adding separate startup-fill costs.
See [fetch-address timing](../research/fetch-address-timing.md) for evidence, examples, and remaining limits.
Data costs use the addresses and widths of actual CPU bus calls, after alignment handling. Data costs use the addresses and widths of actual CPU bus calls, after alignment handling.
BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle. BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle.
External work RAM costs 3 cycles for byte/halfword accesses and 6 for words. External work RAM costs 3 cycles for byte/halfword accesses and 6 for words.
@ -340,7 +346,7 @@ The new settings apply to subsequent code accesses.
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential. 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. 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. A non-refill instruction uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S.
ARM word accesses retain an S cost for their second halfword. 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. Stores and ROM boundaries that already use N do not receive an additional cost.
@ -357,8 +363,9 @@ 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.
- Ordinary code costs use the current instruction address. The ARM open-bus PC+8 snapshot has no fetch timing or startup pipeline fill. - Non-refill code costs follow actual lookahead addresses, but cold current/decode fills have no separate startup charge.
- PC writes use destination-region costs and the restored instruction width for nominal refill accesses. - PC writes still use destination-region costs and the restored instruction width for nominal refill accesses.
Their old-PC fetch and target pair are not yet charged individually; IRQ entry has the same remaining limit.
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles. - Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch. - Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution. - Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.

View file

@ -71,7 +71,8 @@ Audio events and interrupt sources other than timers, display events, DMA comple
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME. `--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`. The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
The handler stops Timer 0, acknowledges IF, increments r10, and returns with `SUBS pc, lr, #4`. The handler stops Timer 0, acknowledges IF, increments r10, and returns with `SUBS pc, lr, #4`.
The demo ends after 21 steps and 153 nominal cycles with one handler call, using default WAITCNT settings. The demo ends after 21 steps and 151 nominal cycles with one handler call, using default WAITCNT settings.
Its initial ARM ROM fetch uses PC+8 sequential timing; cold pipeline filling has no separate startup charge.
These counts test the timing model; they are not hardware timing measurements. These counts test the timing model; they are not hardware timing measurements.
Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention. Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.

View file

@ -172,6 +172,14 @@ Fetch-region boundary research:
See [fetch-region boundary findings](research/fetch-region-boundaries.md) for source agreement, implementation differences, and retained diagnostics. See [fetch-region boundary findings](research/fetch-region-boundaries.md) for source agreement, implementation differences, and retained diagnostics.
No physical-hardware or external-emulator differential result is claimed. No physical-hardware or external-emulator differential result is claimed.
Fetch-address timing research:
- [ARM7TDMI data-operation timing](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/data-operations), for the normal `pc+2L` instruction prefetch in Table 6.4.
- The pinned NanoBoyAdvance `Run` loop above, for the new ARM/Thumb lookahead fetch before current-instruction execution.
- Existing Game Pak wait-state and DMA resume sources above, for applying bus cost and sequence-break rules to the fetched address.
See [fetch-address timing](research/fetch-address-timing.md). This corrects non-refill address selection, not full bus sequencing or refill timing.
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.

View file

@ -70,7 +70,8 @@ Failed-instruction tests continue to assert full-state preservation.
This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline. This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline.
Cold fill and target-pair samples do not add nominal cycles or data accesses. Cold fill and target-pair samples do not add nominal cycles or data accesses.
The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged. The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged.
The current instruction address still selects nominal code cost rather than a separately scheduled fetch address. The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill code cost from the newly sampled address.
Access kinds, cold-fill startup costs, and refill scheduling remain incomplete.
ARM PC+8 open bus and protected BIOS readback remain separate snapshots. ARM PC+8 open bus and protected BIOS readback remain separate snapshots.
The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads. The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads.

View file

@ -45,14 +45,15 @@ A completed DMA WAITCNT store changes the settings before CPU resume.
## Limits ## Limits
The timing model has no per-access fetch scheduling or DMA arbitration. The timing model has no per-access fetch scheduling or DMA arbitration.
The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions without changing these nominal costs. The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions.
It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch. The [fetch-address timing extension](fetch-address-timing.md) now charges non-refill code at PC+4/PC+8 rather than the executing address.
A resumed access selects that fetch region's wait settings. It is still not a separately scheduled bus event.
DMA still executes only between instructions, not between data accesses or during CPU internal cycles. DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
Existing refill summaries remain destination-based `1N+2S` costs. Existing refill summaries remain destination-based `1N+2S` costs.
A resumed branch or exception consumes pending state without adding another nominal access. 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. 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. Stores and fetches at 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. 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. This change does not select a DMA value for CPU open-bus reads.

View file

@ -0,0 +1,102 @@
# Non-refill timing at the actual fetch address
## Finding and evidence
The instruction buffer already sampled ARM P+8 or Thumb P+4 before executing the current instruction at P.
However, `Cpu::step_timed` charged non-refill code at P.
Near a memory-region, ROM wait-window, or 128 KiB page boundary, that selected the wrong bus cost.
It also charged a page-boundary access when execution later reached an already fetched instruction.
The [ARM7TDMI data-operation timing table](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/data-operations) identifies the normal instruction prefetch address as `pc+2L`.
Here L is the instruction length: four bytes in ARM state and two bytes in Thumb state.
An ordinary data operation overlaps this fetch; a register-controlled shift adds an internal cycle.
The [instruction-pipeline description](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) provides the corresponding fetch/decode/execute model.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) provides an implementation cross-check.
Its `Run` method takes the current retained opcode and fetches the next halfword/word at the advanced r15 before execution.
Its refill helpers advance visible r15 by two instruction widths after loading the target pair.
This supports using the new fetch address rather than the current retained opcode's address.
Existing [bus-cost rules](../hardware/cpu.md#instruction-and-bus-timing) supply the region, ROM wait-window, and forced page-boundary costs.
Existing [DMA resume evidence](dma-resume-timing.md) supports the retained one-shot sequence break.
This change uses those rules without claiming complete access-kind sequencing or refill timing.
No physical-hardware measurement or external-emulator differential run was performed.
All programs are original; no external source code or ROM was imported.
## Implemented subset
`Cpu::step_timed` captures the address of `Fetched.lookahead` before execution.
For a non-refill instruction, it passes that address to `bus_cycles` with the incoming instruction width.
The existing instruction summary selects sequential or non-sequential access, with the pending DMA resume override.
WAITCNT is captured before CPU execution, so a CPU store cannot retroactively change its own fetch cost.
The same rule applies to cold and retained pipelines.
Sampling alone adds no data-timing entry or device-clock update.
The returned code cost accounts for one new lookahead access, not an additional copy of it.
Cold current/decode fills still have no separate startup charge.
Successful untimed execution still consumes DMA resume without advancing clocks.
Failed execution retains the existing CPU/buffer, timing, and resume-state rollback rules.
A deferred lookahead error does not fail the current instruction early.
Its nominal code cost uses the attempted fetch address, even when supplied ROM bytes are missing there.
The strict instruction diagnostic occurs only when the missing slot becomes current.
An unmapped region retains the bus-cost helper's one-cycle fallback; this is diagnostic policy, not hardware abort timing.
## Examples with default WAITCNT
| Executing instruction | New fetch | Non-refill code cost |
| --- | --- | --- |
| ARM at `0x02fffff8` | IWRAM `0x03000000` word | 1 cycle, not EWRAM's 6 |
| Thumb at `0x02fffffc` | IWRAM `0x03000000` halfword | 1 cycle, not EWRAM's 3 |
| ARM at `0x07fffffc` | ROM `0x08000004` word | 6 cycles, not OAM's 1 |
| ARM at `0x0801fff8` | ROM page boundary `0x08020000` | 8 cycles for N+S |
| ARM at `0x08020000` | ROM `0x08020008` word | 6 cycles; no repeated page-boundary charge |
For ARM/Thumb ROM words/halfwords, a boundary cost now occurs two executed instruction positions earlier.
Crossing into another wait-state window selects that window's settings at the new fetch.
A completed DMA changes the access kind at this address; it does not append a second code access.
An ordinary ARM instruction starting directly at ROM base now costs 6 code cycles rather than 8.
The charged fetch is at ROM base plus eight, not at the page boundary.
This does not model the full hardware cost of initializing an empty pipeline.
Timer tests and the original timer demo now reflect this bounded rule.
## Deliberately retained limits
- Instruction-local S/N summaries remain in use, including the existing store rule.
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
- Branches, PC loads/writes, SWI, exception returns, and IRQ entry retain destination-based `1N+2S` summaries.
The old-PC fetch and target pair are not yet charged individually.
- The new lookahead cost is not added to those refill summaries; doing so would count an extra access.
- Thumb BL prefixes use their lookahead address; suffixes retain the nominal refill summary.
- Cold fills have no separate startup charge, even though their samples update supported bus history.
- Devices advance once after the whole successful instruction. No per-access timer/IRQ update is introduced.
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
The next timing step is verified source-fetch/target-pair accounting for refills and IRQ entry.
## Original regression coverage
New tests cover:
- ARM/Thumb memory-region crossings with cold and retained pipelines, compared with untimed execution.
- ROM 128 KiB boundaries using independent N/S tables for every wait setting and both prefetch-bit values.
- Wait-window crossings using the newly selected window's settings.
- DMA resume at a cross-region lookahead without duplicate code cost.
- CPU WAITCNT stores using old fetch settings and new settings on the next instruction.
- Deferred missing-lookahead diagnostics and unchanged nominal refill totals.
- Machine timer advancement from the corrected fetch cost before the next IRQ sample.
The address-sensitive tests failed with current-PC charging and pass with the new fetch address.
Existing ROM-start, boundary, BIOS-access, load-alias, and timer assertions were recalculated from PC+8/PC+4 costs.
Data results, access counts, flags, writeback, and failed-step preservation checks remain intact.
## 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 the prior fetch-boundary reports exactly, including cycle counts.
Debug and release reports also match for the same fixture paths.
The original timer demo's nominal total changes from 153 to 151 cycles because its initial ROM fetch is now charged at PC+8.
These are regression checks, not physical-hardware timing conformance results.

View file

@ -76,8 +76,8 @@ A retry takes a new lookahead sample from current memory.
- Protected BIOS history remains separate and image-derived. - Protected BIOS history remains separate and image-derived.
- All data reads within an instruction still use one entry snapshot. - All data reads within an instruction still use one entry snapshot.
Nominal timing remains based on the current instruction address and existing refill summaries. The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
Crossing a wait-state window does not yet schedule code cost at the actual lookahead address. Refill summaries remain destination-based; the instruction-local access kinds and cold-start costs are still nominal.
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete. Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
No boundary timing accuracy is claimed. No boundary timing accuracy is claimed.

View file

@ -55,7 +55,8 @@ Strict instruction reads never use protected BIOS or unused-memory data fallback
- BIOS history commits after successful BIOS instruction execution; target refills do not establish additional BIOS history. - BIOS history commits after successful BIOS instruction execution; target refills do not establish additional BIOS history.
- All unused-memory data reads within an instruction still use its entry snapshot. - All unused-memory data reads within an instruction still use its entry snapshot.
- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point. - DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.
- Current-instruction and destination-based nominal timing remain unchanged. - Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
- Destination-based refill summaries and instruction-local access kinds remain nominal.
- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete. - Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete.
## Original tests ## Original tests

View file

@ -56,7 +56,8 @@ Refill bus history and instruction buffering now consume the same captured targe
Buffer samples add no nominal cycles or data accesses. Buffer samples add no nominal cycles or data accesses.
Timed and untimed execution use the same buffer logic. Timed and untimed execution use the same buffer logic.
Destination-based refill summaries and one-shot non-sequential CPU resume costs remain unchanged. Destination-based refill summaries remain unchanged.
The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill and DMA-resume code costs from the new fetch address.
Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md). Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md).
Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete. Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
Instruction-state-changing compare quirks also remain hardware-unverified. Instruction-state-changing compare quirks also remain hardware-unverified.

View file

@ -45,6 +45,7 @@ Update this file when a feature lands or a limit is removed.
- 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.
- Machine execution to the next VBlank event, with a configurable step limit. - Machine execution to the next VBlank event, with a configurable step limit.
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states. - CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
- Non-refill code costs use actual ARM PC+8/Thumb PC+4 fetch addresses, including memory-region and ROM wait-window boundaries.
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words. - Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
@ -161,7 +162,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
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.
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images. The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
2. Connect instruction buffering to fetch timing, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. 2. Replace destination-only refill summaries with verified source-fetch/target-pair timing, including IRQ entry.
Then extend access-kind sequencing, startup costs, Game Pak prefetch, per-access device updates, and timer/IRQ delays.
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. 3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes. Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.

View file

@ -250,6 +250,11 @@ Integration tests cover CPU/DMA writes, interrupt masks, HALT wake-up, ARM/Thumb
Failed-store tests check register, IRQ-history, CPU, and clock preservation. BIOS reset tests verify retained KEYCNT state. Failed-store tests check register, IRQ-history, CPU, and clock preservation. BIOS reset tests verify retained KEYCNT state.
The bounded graphics frame runner is checked for instruction errors and timeouts. The bounded graphics frame runner is checked for instruction errors and timeouts.
Timing tests cover every ROM wait-state setting, access width, ROM window, and 128 KiB boundary handling. Timing tests cover every ROM wait-state setting, access width, ROM window, and 128 KiB boundary handling.
Fetch-address regressions place executing instructions before memory-region and ROM page/wait-window boundaries.
They check ARM/Thumb widths, cold/retained pipelines, independent N/S tables, DMA resume, and pre-write WAITCNT settings.
Other checks cover deferred fetch errors, unchanged nominal refill totals, timer advancement, and subsequent IRQ sampling.
Startup-ROM and timer expectations now use P+8/P+4 rather than the executing address; data and rollback assertions remain intact.
See [fetch-address timing](research/fetch-address-timing.md) for the bounded scope.
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns. Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers. Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled. Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.