feat(cpu): charge non-refill code at pipeline fetch addresses
This commit is contained in:
parent
0a696ea3e7
commit
0650b60908
26 changed files with 413 additions and 46 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
217
crates/gba-core/src/cpu/fetch_timing_tests.rs
Normal file
217
crates/gba-core/src/cpu/fetch_timing_tests.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/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/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/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
||||
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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:
|
||||
|
||||
- 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.
|
||||
- PC writes use destination-region costs and the restored instruction width for nominal refill accesses.
|
||||
- Non-refill code costs follow actual lookahead addresses, but cold current/decode fills have no separate startup charge.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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 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.
|
||||
Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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 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.
|
||||
The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads.
|
||||
|
|
|
|||
|
|
@ -45,14 +45,15 @@ A completed DMA WAITCNT store changes the settings before CPU resume.
|
|||
## Limits
|
||||
|
||||
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.
|
||||
It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch.
|
||||
The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
This change does not select a DMA value for CPU open-bus reads.
|
||||
|
|
|
|||
102
docs/research/fetch-address-timing.md
Normal file
102
docs/research/fetch-address-timing.md
Normal 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.
|
||||
|
|
@ -76,8 +76,8 @@ A retry takes a new lookahead sample from current memory.
|
|||
- Protected BIOS history remains separate and image-derived.
|
||||
- All data reads within an instruction still use one entry snapshot.
|
||||
|
||||
Nominal timing remains based on the current instruction address and existing refill summaries.
|
||||
Crossing a wait-state window does not yet schedule code cost at the actual lookahead address.
|
||||
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code 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.
|
||||
No boundary timing accuracy is claimed.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Original tests
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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).
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- Machine execution to the next VBlank event, with a configurable step limit.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue