feat(cpu): retain fetch access kinds across instruction execution
This commit is contained in:
parent
2d7eb6e4eb
commit
86321f0071
21 changed files with 637 additions and 61 deletions
|
|
@ -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_sequence_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod fetch_timing_tests;
|
mod fetch_timing_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod instruction_tests;
|
mod instruction_tests;
|
||||||
|
|
@ -176,7 +178,7 @@ impl InstructionSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
|
/// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
|
||||||
/// Clone/equality include buffered fetch results and their instruction-set tag.
|
/// Clone/equality include buffered fetch results, instruction state, and next fetch kind.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Cpu {
|
pub struct Cpu {
|
||||||
registers: [u32; 16],
|
registers: [u32; 16],
|
||||||
|
|
@ -242,7 +244,7 @@ impl Cpu {
|
||||||
) -> Result<(), CpuError> {
|
) -> Result<(), CpuError> {
|
||||||
let pipeline::Fetched {
|
let pipeline::Fetched {
|
||||||
instruction,
|
instruction,
|
||||||
continuation,
|
mut continuation,
|
||||||
lookahead,
|
lookahead,
|
||||||
fill,
|
fill,
|
||||||
} = fetched;
|
} = fetched;
|
||||||
|
|
@ -250,14 +252,15 @@ impl Cpu {
|
||||||
// Always clear the access context, including on diagnostic errors.
|
// Always clear the access context, including on diagnostic errors.
|
||||||
let pc = self.pc();
|
let pc = self.pc();
|
||||||
let instruction_set = self.instruction_set;
|
let instruction_set = self.instruction_set;
|
||||||
let refill = self.instruction_refills(instruction);
|
let summary = self.instruction_summary(instruction);
|
||||||
memory.begin_cpu_access(pc, self.instruction_set, fill.as_ref(), &lookahead);
|
memory.begin_cpu_access(pc, self.instruction_set, fill.as_ref(), &lookahead);
|
||||||
let result = self.execute_instruction(instruction, memory);
|
let result = self.execute_instruction(instruction, memory);
|
||||||
memory.end_cpu_access(result.is_ok());
|
memory.end_cpu_access(result.is_ok());
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
if refill {
|
if summary.refill {
|
||||||
self.refill_pipeline(memory);
|
self.refill_pipeline(memory);
|
||||||
} else {
|
} else {
|
||||||
|
continuation.next_access = summary.next_fetch_kind();
|
||||||
// A state change without an ordinary refill starts a cold sequence.
|
// A state change without an ordinary refill starts a cold sequence.
|
||||||
self.pipeline = if self.instruction_set == instruction_set
|
self.pipeline = if self.instruction_set == instruction_set
|
||||||
&& self.pc() == pc.wrapping_add(instruction_set.width())
|
&& self.pc() == pc.wrapping_add(instruction_set.width())
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,8 @@ fn failed_cpu_fetch_and_data_access_preserve_resume_until_success() {
|
||||||
assert_eq!(timing.code_cycles, 8);
|
assert_eq!(timing.code_cycles, 8);
|
||||||
assert_eq!(timing.data_cycles, 6);
|
assert_eq!(timing.data_cycles, 6);
|
||||||
assert_eq!(timing.internal_cycles, 1);
|
assert_eq!(timing.internal_cycles, 1);
|
||||||
|
// The successful load creates a new sequence break after consuming DMA resume.
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +193,7 @@ fn host_accesses_and_clock_advances_do_not_consume_resume_but_untimed_cpu_does()
|
||||||
fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_charging() {
|
fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_charging() {
|
||||||
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 uses DMA's N, then breaks the following fetch.
|
||||||
(ROM_START + 0x20000 - 8, 0xe1a0_0000, 0), // PC+8 fetch 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);
|
||||||
|
|
@ -199,6 +201,10 @@ fn skipped_conditions_stores_and_boundaries_consume_resume_without_double_chargi
|
||||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||||
assert_eq!(timing.code_cycles, 8);
|
assert_eq!(timing.code_cycles, 8);
|
||||||
assert_eq!(timing.data_cycles, data);
|
assert_eq!(timing.data_cycles, data);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut bus).unwrap().code_cycles,
|
||||||
|
if data == 6 { 8 } else { 6 }
|
||||||
|
);
|
||||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +216,7 @@ fn waitcnt_cpu_store_uses_old_code_cost_but_dma_store_applies_before_resume() {
|
||||||
cpu.registers[1] = WAITCNT;
|
cpu.registers[1] = WAITCNT;
|
||||||
dma(&mut bus);
|
dma(&mut bus);
|
||||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 8);
|
||||||
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 5); // Store breaks the next fetch; new N=3, S=2.
|
||||||
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 4);
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 4);
|
||||||
|
|
||||||
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);
|
||||||
|
|
|
||||||
381
crates/gba-core/src/cpu/fetch_sequence_tests.rs
Normal file
381
crates/gba-core/src/cpu/fetch_sequence_tests.rs
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
//! Original instruction-pair tests. Expected access kinds do not use CPU summaries.
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
dma::{DMA_BASE, DMA_STRIDE},
|
||||||
|
io::{HALTCNT, IE, IME, TIMER_BASE, WAITCNT},
|
||||||
|
machine::{Machine, StepKind},
|
||||||
|
memory::{BIOS_SIZE, ROM_CAPACITY, ROM_START},
|
||||||
|
timing::AccessKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PC: u32 = ROM_START + 0x100;
|
||||||
|
const DATA: u32 = 0x0300_1000;
|
||||||
|
const ARM_NOP: u32 = 0xe1a0_0000;
|
||||||
|
const THUMB_NOP: u32 = 0x46c0;
|
||||||
|
|
||||||
|
fn program(thumb: bool, pc: u32, instructions: &[u32]) -> (Cpu, Memory) {
|
||||||
|
let width = if thumb { 2 } else { 4 };
|
||||||
|
let pattern = if thumb { 0x46c0_46c0 } else { ARM_NOP };
|
||||||
|
let offset = (pc & 0x01ff_ffff) as usize;
|
||||||
|
let length = (offset + 4 * width).clamp(0x20020, ROM_CAPACITY);
|
||||||
|
let mut rom = pattern.to_le_bytes().repeat(length.div_ceil(4));
|
||||||
|
for (index, instruction) in instructions.iter().enumerate() {
|
||||||
|
rom[offset + index * width..offset + (index + 1) * width]
|
||||||
|
.copy_from_slice(&instruction.to_le_bytes()[..width]);
|
||||||
|
}
|
||||||
|
let mut cpu = Cpu::new(pc);
|
||||||
|
if thumb {
|
||||||
|
cpu.instruction_set = InstructionSet::Thumb;
|
||||||
|
}
|
||||||
|
cpu.registers[0] = 7;
|
||||||
|
cpu.registers[1] = DATA;
|
||||||
|
cpu.registers[13] = DATA + 64;
|
||||||
|
(
|
||||||
|
cpu,
|
||||||
|
Memory::with_bios(rom, ARM_NOP.to_le_bytes().repeat(BIOS_SIZE / 4)).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arm_instruction_effects_select_the_following_fetch_not_their_own() {
|
||||||
|
for (instruction, breaks) in [
|
||||||
|
(ARM_NOP, false),
|
||||||
|
(0xe1a0_0082, false), // Immediate shift.
|
||||||
|
(0xe10f_0000, false),
|
||||||
|
(0xe321_f01f, false), // MRS / MSR.
|
||||||
|
(0x0591_0000, false),
|
||||||
|
(0x0581_0000, false), // Skipped load/store.
|
||||||
|
(0x0000_0291, false),
|
||||||
|
(0x01a0_0312, false), // Skipped multiply/shift.
|
||||||
|
(0xe591_0000, true),
|
||||||
|
(0xe581_0000, true), // Word load/store.
|
||||||
|
(0xe5d1_0000, true),
|
||||||
|
(0xe5c1_0000, true), // Byte load/store.
|
||||||
|
(0xe1d1_00b0, true),
|
||||||
|
(0xe1c1_00b0, true), // Halfword load/store.
|
||||||
|
(0xe1d1_00d0, true),
|
||||||
|
(0xe1d1_00f0, true), // Signed byte/halfword.
|
||||||
|
(0xe891_0005, true),
|
||||||
|
(0xe881_0005, true), // Block load/store.
|
||||||
|
(0xe881_0000, true), // Empty block store.
|
||||||
|
(0xe101_0092, true),
|
||||||
|
(0xe141_0092, true), // SWP/SWPB.
|
||||||
|
(0xe1a0_0312, true), // Register shift, zero shift amount.
|
||||||
|
(0xe000_0392, true),
|
||||||
|
(0xe020_4392, true), // MUL/MLA.
|
||||||
|
(0xe085_4392, true),
|
||||||
|
(0xe0a5_4392, true), // UMULL/UMLAL.
|
||||||
|
(0xe0c5_4392, true),
|
||||||
|
(0xe0e5_4392, true), // SMULL/SMLAL.
|
||||||
|
] {
|
||||||
|
check_pair(false, instruction, breaks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thumb_instruction_effects_select_the_following_fetch_not_their_own() {
|
||||||
|
for (instruction, breaks) in [
|
||||||
|
(THUMB_NOP, false),
|
||||||
|
(0x0040, false),
|
||||||
|
(0x3001, false),
|
||||||
|
(0x4008, false),
|
||||||
|
(0xb001, false),
|
||||||
|
(0xf000, false), // AND, SP adjust, BL prefix.
|
||||||
|
(0xd000, false), // Skipped BEQ (Z clear).
|
||||||
|
(0x4800, true), // Literal load.
|
||||||
|
(0x6008, true),
|
||||||
|
(0x6808, true),
|
||||||
|
(0x7008, true),
|
||||||
|
(0x7808, true),
|
||||||
|
(0x8008, true),
|
||||||
|
(0x8808, true),
|
||||||
|
(0x9000, true),
|
||||||
|
(0x9800, true),
|
||||||
|
(0x5088, true),
|
||||||
|
(0x5288, true),
|
||||||
|
(0x5488, true),
|
||||||
|
(0x5688, true),
|
||||||
|
(0x5888, true),
|
||||||
|
(0x5a88, true),
|
||||||
|
(0x5c88, true),
|
||||||
|
(0x5e88, true),
|
||||||
|
(0xb405, true),
|
||||||
|
(0xbc05, true),
|
||||||
|
(0xc105, true),
|
||||||
|
(0xc905, true),
|
||||||
|
(0xb400, true),
|
||||||
|
(0xc100, true), // Empty stores.
|
||||||
|
(0x4090, true),
|
||||||
|
(0x40d0, true),
|
||||||
|
(0x4110, true),
|
||||||
|
(0x41d0, true), // Register shifts by r2=0.
|
||||||
|
(0x4348, true), // MUL.
|
||||||
|
] {
|
||||||
|
check_pair(true, instruction, breaks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_pair(thumb: bool, instruction: u32, breaks: bool) {
|
||||||
|
for window in 0..3 {
|
||||||
|
let (cpu, mut memory) = program(thumb, PC + window as u32 * 0x0200_0000, &[instruction]);
|
||||||
|
for first in 0..4 {
|
||||||
|
for second in 0..2 {
|
||||||
|
for prefetch in [0, 0x4000] {
|
||||||
|
memory
|
||||||
|
.write16(
|
||||||
|
WAITCNT,
|
||||||
|
(first << [2, 5, 8][window])
|
||||||
|
| (second << [4, 7, 10][window])
|
||||||
|
| prefetch,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let n = [5, 4, 3, 9][first as usize];
|
||||||
|
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||||
|
let extra = if thumb { 0 } else { s };
|
||||||
|
let mut cpu = cpu.clone();
|
||||||
|
// Cold startup keeps the existing nominal S policy.
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
s + extra,
|
||||||
|
"first fetch for {instruction:08x}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if breaks { n + extra } else { s + extra },
|
||||||
|
"following fetch for {instruction:08x}"
|
||||||
|
);
|
||||||
|
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s + extra);
|
||||||
|
assert_eq!(memory.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timed_and_untimed_execution_and_clones_retain_the_same_next_fetch_kind() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let instruction = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||||
|
let (mut timed, mut a) = program(thumb, PC, &[instruction]);
|
||||||
|
let (mut plain, mut b) = program(thumb, PC, &[instruction]);
|
||||||
|
timed.step_timed(&mut a).unwrap();
|
||||||
|
plain.step(&mut b).unwrap();
|
||||||
|
assert_eq!(timed, plain);
|
||||||
|
let mut copy = plain.clone();
|
||||||
|
for expected in [if thumb { 5 } else { 8 }, if thumb { 3 } else { 6 }] {
|
||||||
|
assert_eq!(timed.step_timed(&mut a).unwrap().code_cycles, expected);
|
||||||
|
assert_eq!(copy.step_timed(&mut b).unwrap().code_cycles, expected);
|
||||||
|
assert_eq!(timed, copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn following_transfers_branches_and_skipped_conditions_consume_the_prior_sequence_break() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let load = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||||
|
let store = if thumb { 0x6008 } else { 0xe581_0000 };
|
||||||
|
let branch = if thumb { 0xe000 } else { 0xea00_0000 };
|
||||||
|
let skipped = if thumb { 0xd000 } else { 0x0581_0000 };
|
||||||
|
for second in [load, store, branch, skipped] {
|
||||||
|
let (mut cpu, mut memory) = program(thumb, PC, &[load, second]);
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
let cost = cpu.step_timed(&mut memory).unwrap();
|
||||||
|
let n = if thumb { 5 } else { 8 };
|
||||||
|
let s = if thumb { 3 } else { 6 };
|
||||||
|
assert_eq!(
|
||||||
|
cost.code_cycles,
|
||||||
|
if second == branch { n + n + s } else { n }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if second == branch || second == skipped {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
n
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pc_load_refills_restore_sequential_fetches_after_data_and_internal_cycles() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let instruction = if thumb { 0xbd00 } else { 0xe591_f000 };
|
||||||
|
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||||
|
memory
|
||||||
|
.write32(if thumb { DATA + 64 } else { DATA }, PC + 0x20)
|
||||||
|
.unwrap();
|
||||||
|
let cost = cpu.step_timed(&mut memory).unwrap();
|
||||||
|
assert_eq!(cost.code_cycles, if thumb { 11 } else { 20 });
|
||||||
|
assert_eq!(cost.data_cycles, 1);
|
||||||
|
assert_eq!(cost.internal_cycles, 1);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if thumb { 3 } else { 6 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dma_override_coalesces_with_cpu_history_and_does_not_replay_the_penalty() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
for instruction in [
|
||||||
|
if thumb { 0x6808 } else { 0xe591_0000 },
|
||||||
|
if thumb { THUMB_NOP } else { ARM_NOP },
|
||||||
|
] {
|
||||||
|
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||||
|
memory.write32(dma, DATA).unwrap();
|
||||||
|
memory.write32(dma + 4, DATA + 128).unwrap();
|
||||||
|
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||||
|
memory.step_dma().unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if thumb { 5 } else { 8 }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if thumb { 3 } else { 6 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irq_uses_retained_incoming_kind_before_invalidating_the_interrupted_buffer() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
for instruction in [
|
||||||
|
if thumb { 0x6808 } else { 0xe591_0000 },
|
||||||
|
if thumb { THUMB_NOP } else { ARM_NOP },
|
||||||
|
] {
|
||||||
|
let (mut cpu, mut memory) = program(thumb, PC, &[instruction]);
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
memory.write16(IE, 8).unwrap();
|
||||||
|
memory.write16(IME, 1).unwrap();
|
||||||
|
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||||
|
memory.advance_cycles(1);
|
||||||
|
let n = instruction != if thumb { THUMB_NOP } else { ARM_NOP };
|
||||||
|
let expected = match (thumb, n) {
|
||||||
|
(true, true) => 7,
|
||||||
|
(true, false) => 5,
|
||||||
|
(false, true) => 10,
|
||||||
|
(false, false) => 8,
|
||||||
|
};
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
let start = machine.cycles();
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, expected);
|
||||||
|
assert_eq!(machine.cycles() - start, u64::from(expected));
|
||||||
|
assert_eq!(machine.cpu().next_fetch_kind(), AccessKind::Sequential);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.last_timing().code_cycles, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_execution_retains_next_kind_and_retry_does_not_consume_it() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let load = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||||
|
let (mut cpu, mut memory) = program(thumb, PC, &[load, load]);
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
cpu.registers[1] = 0x0e00_0000;
|
||||||
|
let before = cpu.clone();
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert!(cpu.step_timed(&mut memory).is_err());
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
assert_eq!(cpu.next_fetch_kind(), AccessKind::NonSequential);
|
||||||
|
}
|
||||||
|
cpu.registers[1] = DATA;
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if thumb { 5 } else { 8 }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
|
if thumb { 5 } else { 8 }
|
||||||
|
);
|
||||||
|
assert_eq!(memory.cycles(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_current_slot_and_masked_irq_preserve_the_pending_cpu_sequence() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let instruction: u32 = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||||
|
let width = if thumb { 2 } else { 4 };
|
||||||
|
let mut memory = Memory::new(instruction.to_le_bytes()[..width].to_vec()).unwrap();
|
||||||
|
let mut cpu = Cpu::new(ROM_START);
|
||||||
|
if thumb {
|
||||||
|
cpu.instruction_set = InstructionSet::Thumb;
|
||||||
|
}
|
||||||
|
cpu.registers[1] = DATA;
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
assert_eq!(cpu.next_fetch_kind(), AccessKind::NonSequential);
|
||||||
|
let before = cpu.clone();
|
||||||
|
assert!(cpu.step_timed(&mut memory).is_err());
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
memory.write16(IE, 8).unwrap();
|
||||||
|
memory.write16(IME, 1).unwrap();
|
||||||
|
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||||
|
memory.advance_cycles(1);
|
||||||
|
cpu.irq_disabled = true;
|
||||||
|
let before = cpu.clone();
|
||||||
|
assert_eq!(cpu.take_irq_timed(&mut memory), None);
|
||||||
|
assert_eq!(cpu, before);
|
||||||
|
// A debugger PC discontinuity cannot reuse the old sequence's access kind.
|
||||||
|
cpu.registers[15] = PC;
|
||||||
|
assert_eq!(cpu.next_fetch_kind(), AccessKind::Sequential);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_accesses_and_halt_idle_preserve_kind_but_debugger_invalidation_starts_cold() {
|
||||||
|
let (mut cpu, mut memory) = program(false, PC, &[0xe591_0000]);
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
memory.read32(DATA).unwrap();
|
||||||
|
memory.write32(DATA, 1).unwrap();
|
||||||
|
memory.advance_cycles(7);
|
||||||
|
memory.write8(HALTCNT, 0).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
|
||||||
|
assert_eq!(machine.cpu().next_fetch_kind(), AccessKind::NonSequential);
|
||||||
|
let mut cpu = machine.cpu().clone();
|
||||||
|
cpu.invalidate_pipeline();
|
||||||
|
assert_eq!(cpu.next_fetch_kind(), AccessKind::Sequential);
|
||||||
|
assert_eq!(cpu.step_timed(machine.memory_mut()).unwrap().code_cycles, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_and_wait_window_crossings_apply_the_retained_kind_at_the_new_fetch_address() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let width = if thumb { 2 } else { 4 };
|
||||||
|
for (boundary, waitcnt, n, s) in [
|
||||||
|
(
|
||||||
|
ROM_START + 0x20000,
|
||||||
|
0,
|
||||||
|
if thumb { 5 } else { 8 },
|
||||||
|
if thumb { 3 } else { 6 },
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0x0a00_0000,
|
||||||
|
0x80,
|
||||||
|
if thumb { 5 } else { 7 },
|
||||||
|
if thumb { 2 } else { 4 },
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let (mut cpu, mut memory) = program(
|
||||||
|
thumb,
|
||||||
|
boundary - 3 * width,
|
||||||
|
&[if thumb { 0x6808 } else { 0xe591_0000 }],
|
||||||
|
);
|
||||||
|
memory.write16(WAITCNT, waitcnt).unwrap();
|
||||||
|
cpu.step(&mut memory).unwrap();
|
||||||
|
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, n);
|
||||||
|
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -164,9 +164,10 @@ fn a_boundary_waitcnt_store_uses_old_fetch_settings_and_the_next_fetch_uses_new_
|
||||||
cpu.registers[0] = 0xc0; // WS1 N=3, S=2.
|
cpu.registers[0] = 0xc0; // WS1 N=3, S=2.
|
||||||
cpu.registers[1] = WAITCNT;
|
cpu.registers[1] = WAITCNT;
|
||||||
let cost = cpu.step_timed(&mut memory).unwrap();
|
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.code_cycles, 10); // Old WS1 S+S=10 at lookahead 0x0a000004.
|
||||||
assert_eq!(cost.data_cycles, 1);
|
assert_eq!(cost.data_cycles, 1);
|
||||||
assert_eq!(memory.waitcnt(), 0xc0);
|
assert_eq!(memory.waitcnt(), 0xc0);
|
||||||
|
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 5); // Store's next fetch: new N+S.
|
||||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 4);
|
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
//! ARM/Thumb instruction buffering. Timing and bus ownership remain separate.
|
//! ARM/Thumb instruction buffering and the next CPU fetch access kind.
|
||||||
use super::{Cpu, CpuError, InstructionSet};
|
use super::{Cpu, CpuError, InstructionSet};
|
||||||
use crate::memory::{InstructionFetch, Memory, MemoryError};
|
use crate::{
|
||||||
|
memory::{InstructionFetch, Memory, MemoryError},
|
||||||
|
timing::AccessKind,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(super) struct Pipeline {
|
pub(super) struct Pipeline {
|
||||||
|
|
@ -9,6 +12,7 @@ pub(super) struct Pipeline {
|
||||||
// Deferred strict-fetch results for the next execute and decode positions.
|
// Deferred strict-fetch results for the next execute and decode positions.
|
||||||
// Thumb halfwords are zero-extended; their upper bits are not another opcode.
|
// Thumb halfwords are zero-extended; their upper bits are not another opcode.
|
||||||
instructions: [Result<u32, MemoryError>; 2],
|
instructions: [Result<u32, MemoryError>; 2],
|
||||||
|
pub(super) next_access: AccessKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct Fetched {
|
pub(super) struct Fetched {
|
||||||
|
|
@ -21,11 +25,21 @@ pub(super) struct Fetched {
|
||||||
impl Cpu {
|
impl Cpu {
|
||||||
/// Discard buffered instructions after debugger code edits or rebinding memory.
|
/// Discard buffered instructions after debugger code edits or rebinding memory.
|
||||||
/// Ordinary CPU/DMA stores and host inspection must not call this automatically.
|
/// Ordinary CPU/DMA stores and host inspection must not call this automatically.
|
||||||
/// The next step fills from its current PC/state without adding nominal cycles.
|
/// The next step uses nominal S startup with no separate fill charge.
|
||||||
|
/// This also discards the prior CPU fetch kind, but not Memory's DMA override.
|
||||||
pub fn invalidate_pipeline(&mut self) {
|
pub fn invalidate_pipeline(&mut self) {
|
||||||
self.pipeline = None;
|
self.pipeline = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cold/debugger entry keeps the existing nominal S startup policy.
|
||||||
|
/// A mismatched PC/state cannot reuse another sequence's access kind.
|
||||||
|
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||||
|
self.pipeline
|
||||||
|
.as_ref()
|
||||||
|
.filter(|pipe| pipe.pc == self.pc() && pipe.instruction_set == self.instruction_set)
|
||||||
|
.map_or(AccessKind::Sequential, |pipe| pipe.next_access)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn fetch(&self, memory: &Memory) -> Result<Fetched, CpuError> {
|
pub(super) fn fetch(&self, memory: &Memory) -> Result<Fetched, CpuError> {
|
||||||
let pc = self.pc();
|
let pc = self.pc();
|
||||||
let instruction_set = self.instruction_set;
|
let instruction_set = self.instruction_set;
|
||||||
|
|
@ -53,6 +67,7 @@ impl Cpu {
|
||||||
pc: pc.wrapping_add(width),
|
pc: pc.wrapping_add(width),
|
||||||
instruction_set,
|
instruction_set,
|
||||||
instructions: [decode, fetched.instruction.clone()],
|
instructions: [decode, fetched.instruction.clone()],
|
||||||
|
next_access: AccessKind::Sequential, // Set from execution only on success.
|
||||||
},
|
},
|
||||||
lookahead: fetched,
|
lookahead: fetched,
|
||||||
fill,
|
fill,
|
||||||
|
|
@ -73,6 +88,7 @@ impl Cpu {
|
||||||
pc,
|
pc,
|
||||||
instruction_set,
|
instruction_set,
|
||||||
instructions: fetches.map(|fetch| fetch.instruction),
|
instructions: fetches.map(|fetch| fetch.instruction),
|
||||||
|
next_access: AccessKind::Sequential,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
||||||
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
|
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
|
||||||
//! Source access kinds remain instruction-local nominal summaries.
|
//! Successful execution records the next fetch kind in the instruction buffer.
|
||||||
|
|
||||||
use super::{Cpu, CpuError, InstructionSet};
|
use super::{Cpu, CpuError, InstructionSet};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -9,13 +9,23 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Summary {
|
pub(super) struct Summary {
|
||||||
code_kind: AccessKind,
|
data: bool,
|
||||||
refill: bool,
|
pub(super) refill: bool,
|
||||||
internal: u32,
|
internal: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Summary {
|
impl Summary {
|
||||||
|
/// Data accesses and internal cycles break the following code sequence.
|
||||||
|
/// A refill ends with a sequential target fetch and starts a new sequence.
|
||||||
|
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||||
|
if !self.refill && (self.data || self.internal != 0) {
|
||||||
|
AccessKind::NonSequential
|
||||||
|
} else {
|
||||||
|
AccessKind::Sequential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn branch() -> Self {
|
fn branch() -> Self {
|
||||||
Self {
|
Self {
|
||||||
refill: true,
|
refill: true,
|
||||||
|
|
@ -25,11 +35,7 @@ impl Summary {
|
||||||
|
|
||||||
fn transfer(load: bool, pc: bool) -> Self {
|
fn transfer(load: bool, pc: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
code_kind: if load {
|
data: true,
|
||||||
AccessKind::Sequential
|
|
||||||
} else {
|
|
||||||
AccessKind::NonSequential
|
|
||||||
},
|
|
||||||
refill: load && pc,
|
refill: load && pc,
|
||||||
internal: u32::from(load),
|
internal: u32::from(load),
|
||||||
}
|
}
|
||||||
|
|
@ -52,15 +58,12 @@ impl Cpu {
|
||||||
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
|
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
|
||||||
let fetched = self.fetch(memory)?;
|
let fetched = self.fetch(memory)?;
|
||||||
let instruction = fetched.instruction;
|
let instruction = fetched.instruction;
|
||||||
let summary = match self.instruction_set {
|
let summary = self.instruction_summary(instruction);
|
||||||
InstructionSet::Arm => self.arm_summary(instruction),
|
|
||||||
InstructionSet::Thumb => self.thumb_summary(instruction),
|
|
||||||
};
|
|
||||||
let fetch_address = fetched.lookahead.address();
|
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();
|
||||||
let code_kind = memory.cpu_code_kind(summary.code_kind);
|
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||||
memory.begin_data_timing();
|
memory.begin_data_timing();
|
||||||
let result = self.execute_fetched(fetched, memory);
|
let result = self.execute_fetched(fetched, memory);
|
||||||
let data_cycles = memory.end_data_timing();
|
let data_cycles = memory.end_data_timing();
|
||||||
|
|
@ -82,11 +85,11 @@ impl Cpu {
|
||||||
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||||
let state = self.instruction_set;
|
let state = self.instruction_set;
|
||||||
let address = self.pc().wrapping_add(2 * state.width());
|
let address = self.pc().wrapping_add(2 * state.width());
|
||||||
|
let kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||||
if !self.take_interrupt(memory.irq_pending(), false) {
|
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let waitcnt = memory.waitcnt();
|
let waitcnt = memory.waitcnt();
|
||||||
let kind = memory.cpu_code_kind(AccessKind::Sequential);
|
|
||||||
let fetch = memory.fetch_instruction(address, state);
|
let fetch = memory.fetch_instruction(address, state);
|
||||||
memory.discarded_cpu_fetch(&fetch);
|
memory.discarded_cpu_fetch(&fetch);
|
||||||
self.refill_pipeline(memory);
|
self.refill_pipeline(memory);
|
||||||
|
|
@ -122,6 +125,7 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
|
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
|
||||||
return Summary {
|
return Summary {
|
||||||
|
data: true,
|
||||||
internal: 1,
|
internal: 1,
|
||||||
..Summary::default()
|
..Summary::default()
|
||||||
}; // SWP: two N data accesses
|
}; // SWP: two N data accesses
|
||||||
|
|
@ -158,12 +162,12 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Share the existing control-flow classification with bounded bus history.
|
// Classify before instruction effects in both timed and untimed execution.
|
||||||
// A PC write can refill even when its target equals the sequential address.
|
// A PC write can refill even when its target equals the sequential address.
|
||||||
pub(super) fn instruction_refills(&self, instruction: u32) -> bool {
|
pub(super) fn instruction_summary(&self, instruction: u32) -> Summary {
|
||||||
match self.instruction_set {
|
match self.instruction_set {
|
||||||
InstructionSet::Arm => self.arm_summary(instruction).refill,
|
InstructionSet::Arm => self.arm_summary(instruction),
|
||||||
InstructionSet::Thumb => self.thumb_summary(instruction).refill,
|
InstructionSet::Thumb => self.thumb_summary(instruction),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,9 +338,9 @@ fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
|
||||||
let mut cpu = Cpu::new(ROM_START);
|
let mut cpu = Cpu::new(ROM_START);
|
||||||
cpu.registers[0] = WAITCNT;
|
cpu.registers[0] = WAITCNT;
|
||||||
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
|
cpu.registers[1] = 0x18; // N=3 and S=2 per halfword
|
||||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(8, 1, 0));
|
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(6, 1, 0)); // Incoming S at old WAITCNT.
|
||||||
assert_eq!(memory.waitcnt(), 0x18);
|
assert_eq!(memory.waitcnt(), 0x18);
|
||||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(4, 0, 0));
|
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(5, 0, 0)); // Following N at new WAITCNT.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ fn timer_demo_configures_hardware_in_cpu_code_and_handles_exactly_one_irq() {
|
||||||
assert_eq!(machine.cpu().registers()[10], 1);
|
assert_eq!(machine.cpu().registers()[10], 1);
|
||||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
||||||
// IRQ entry gains five cycles before the timer stops; return loses five afterward.
|
// Persistent fetch kinds move costs between setup instructions and the running timer.
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff9);
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,13 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||||
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
||||||
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns |
|
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns |
|
||||||
| `crates/gba-core/src/cpu/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, next fetch kind, deferred diagnostics, refills, and 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` | Source-fetch/target-pair costs, IRQ fetch integration, nominal access kinds, and data accounting |
|
| `crates/gba-core/src/cpu/timing.rs` | Source-fetch/target-pair costs, IRQ integration, next-fetch classification, and data 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/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/refill_timing_tests.rs` | Refill source/target widths, wait settings, boundaries, DMA resume, and IRQ diagnostics |
|
| `crates/gba-core/src/cpu/refill_timing_tests.rs` | Refill source/target widths, wait settings, boundaries, DMA resume, and IRQ diagnostics |
|
||||||
|
| `crates/gba-core/src/cpu/fetch_sequence_tests.rs` | Instruction-pair access kinds, transfer/internal-cycle breaks, refills, IRQ/DMA composition, and rollback |
|
||||||
| `crates/gba-core/src/memory/irq_fetch_tests.rs` | Discarded IRQ fetch lanes, cold/retained entry, BIOS-history isolation, and deferred vector errors |
|
| `crates/gba-core/src/memory/irq_fetch_tests.rs` | Discarded IRQ fetch lanes, cold/retained entry, BIOS-history isolation, and deferred vector errors |
|
||||||
| `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 |
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,18 @@ A buffered fetch error fails only if execution reaches that slot. No automatic h
|
||||||
A failed instruction preserves all CPU state, including the old buffer; its speculative advance is discarded.
|
A failed instruction preserves all CPU state, including the old buffer; its speculative advance is discarded.
|
||||||
A successful branch discards abandoned-path errors and replaces them with target fetch results.
|
A successful branch discards abandoned-path errors and replaces them with target fetch results.
|
||||||
|
|
||||||
CPU clones and equality include the buffer. Architectural state checks are separate from full-state rollback checks in tests.
|
CPU clones and equality include the buffer and its next fetch access kind.
|
||||||
|
Architectural state checks are separate from full-state rollback checks in tests.
|
||||||
`Cpu::invalidate_pipeline()` explicitly discards buffered instructions without changing registers, flags, or device clocks.
|
`Cpu::invalidate_pipeline()` explicitly discards buffered instructions without changing registers, flags, or device clocks.
|
||||||
Use it after debugger code repair or when attaching different memory to a retained CPU.
|
Use it after debugger code repair or when attaching different memory to a retained CPU.
|
||||||
|
Invalidation also resets the CPU fetch kind to the nominal cold-start S policy; Memory's DMA override remains separate.
|
||||||
Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code behavior.
|
Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code behavior.
|
||||||
|
|
||||||
This is instruction buffering, not a complete timed fetch pipeline.
|
This is instruction buffering, not a complete timed fetch pipeline.
|
||||||
Sampling adds no data accesses or independent device-clock updates.
|
Sampling adds no data accesses or independent device-clock updates.
|
||||||
Code timing charges the source fetch and target pair once, with a DMA resume override on the source access.
|
Code timing charges the source fetch and target pair once, with a DMA resume override on the source access.
|
||||||
Instruction buffering and supported bus history now consume the same mapped fetch samples.
|
Instruction buffering and supported bus history now consume the same mapped fetch samples.
|
||||||
Complete fetch-driven bus history, access-kind sequencing, per-access device updates, and DMA arbitration remain incomplete.
|
Complete fetch-driven bus history, per-access device updates, and DMA arbitration remain incomplete.
|
||||||
See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md), [Thumb extension](../research/thumb-instruction-buffer.md),
|
See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md), [Thumb extension](../research/thumb-instruction-buffer.md),
|
||||||
and [shared fetch samples](../research/shared-fetch-samples.md).
|
and [shared fetch samples](../research/shared-fetch-samples.md).
|
||||||
|
|
||||||
|
|
@ -287,7 +289,7 @@ ARM `TST`, `TEQ`, `CMP`, and `CMN` encodings with unused destination bits set to
|
||||||
These compare/status forms do not write the arithmetic result to PC. Execution continues at the next sequential address.
|
These compare/status forms do not write the arithmetic result to PC. Execution continues at the next sequential address.
|
||||||
In User/System modes, they perform ordinary test/compare flag updates without a mode change.
|
In User/System modes, they perform ordinary test/compare flag updates without a mode change.
|
||||||
Invalid saved modes still produce an atomic diagnostic. Failed conditions do not restore or validate status.
|
Invalid saved modes still produce an atomic diagnostic. Failed conditions do not restore or validate status.
|
||||||
Nominal timing remains one sequential code access, plus an internal cycle for register-specified shifts; there is no refill cost.
|
Nominal timing uses the incoming code access kind, plus an internal cycle for register-specified shifts; there is no refill cost.
|
||||||
This fixes the mode-switch expectation in public `jsmolka/gba-tests` ARM test 234 and agrees with mGBA's shared ALU flag handling.
|
This fixes the mode-switch expectation in public `jsmolka/gba-tests` ARM test 234 and agrees with mGBA's shared ALU flag handling.
|
||||||
Instruction buffering is discarded on a state change without an ordinary refill.
|
Instruction buffering is discarded on a state change without an ordinary refill.
|
||||||
Instruction-state-changing compare forms and exact pipeline behavior remain hardware-unverified.
|
Instruction-state-changing compare forms and exact pipeline behavior remain hardware-unverified.
|
||||||
|
|
@ -311,29 +313,35 @@ Neither constructor provides Nintendo BIOS services or initializes BIOS-managed
|
||||||
|
|
||||||
## Instruction and bus timing
|
## Instruction and bus timing
|
||||||
|
|
||||||
The timing model uses ARM7 cycle summaries:
|
The timing model distinguishes these ARM7 cycle types:
|
||||||
|
|
||||||
- **S:** sequential memory access.
|
- **S:** sequential memory access.
|
||||||
- **N:** non-sequential memory access.
|
- **N:** non-sequential memory access.
|
||||||
- **I:** internal CPU cycle, always one clock cycle.
|
- **I:** internal CPU cycle, always one clock cycle.
|
||||||
|
|
||||||
An ordinary arithmetic instruction costs 1S. A register-specified shift adds 1I, even for a zero shift amount.
|
Every instruction charges its source fetch using the incoming access kind retained by the CPU buffer.
|
||||||
Loads cost a code S access, a data N access, and 1I. Stores use N for code and data.
|
Ordinary arithmetic and immediate shifts leave the next fetch S.
|
||||||
Block transfers charge N for the first data word and S for each following word.
|
Register-specified shifts add 1I, even for a zero shift amount, and leave the next fetch N.
|
||||||
Loads add 1I. Loading PC also adds the branch refill cost.
|
Loads and stores start their data accesses with N and leave the next source fetch N.
|
||||||
|
Block transfers use N for the first data word and S for each following word.
|
||||||
|
Loads add 1I. Loading PC also fetches a target pair, which restores S for subsequent target execution.
|
||||||
Branches, PC writes, software interrupts, and IRQ entry charge one source fetch plus a target N+S pair.
|
Branches, PC writes, software interrupts, and IRQ entry charge one source fetch plus a target N+S pair.
|
||||||
The source fetch uses the incoming state at P+2W. The target pair uses the resulting state at T and T+W.
|
The source fetch uses the incoming state at P+2W. The target pair uses the resulting state at T and T+W.
|
||||||
The first target instruction charges T+2W when it executes; the refill does not charge it early.
|
The first target instruction charges T+2W when it executes; the refill does not charge it early.
|
||||||
A skipped conditional instruction only pays its code S access.
|
A skipped conditional instruction only pays its incoming code access and leaves the following fetch S.
|
||||||
Thumb `BL` charges one source fetch for each half; only its suffix adds the target N+S pair.
|
Thumb `BL` charges one source fetch for each half; only its suffix adds the target N+S pair.
|
||||||
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.
|
||||||
|
Multiply internal cycles leave the following fetch N. Swaps also leave N after their data/internal work.
|
||||||
|
Successful target refills end with S, overriding sequence breaks from preceding data/internal work.
|
||||||
|
IRQ entry uses the interrupted buffer's incoming kind before replacing that buffer.
|
||||||
|
|
||||||
Source code costs use the new instruction fetch address: P+8 for ARM and P+4 for Thumb.
|
Source 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.
|
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.
|
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.
|
This applies to cold and retained pipelines without adding separate startup-fill costs.
|
||||||
See [fetch-address timing](../research/fetch-address-timing.md) and [refill timing](../research/refill-fetch-timing.md) for evidence and limits.
|
See [fetch-address timing](../research/fetch-address-timing.md), [refill timing](../research/refill-fetch-timing.md),
|
||||||
|
and [persistent access kinds](../research/fetch-access-sequencing.md) for evidence and 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.
|
||||||
|
|
@ -355,7 +363,8 @@ This applies even when DMA only accesses RAM. Consecutive units retain one pendi
|
||||||
The source fetch uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S.
|
The source fetch uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S.
|
||||||
This includes branches and accepted IRQ entry; target pairs still use N then S.
|
This includes branches and accepted IRQ entry; target pairs still use N then 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.
|
CPU history and ROM boundaries that already require N do not receive an additional access.
|
||||||
|
A completed load or store can establish a new N requirement after consuming the DMA override.
|
||||||
|
|
||||||
Successful timed or untimed CPU execution consumes pending resume, including skipped conditions and internal-memory instructions.
|
Successful timed or untimed CPU execution consumes pending resume, including skipped conditions and internal-memory instructions.
|
||||||
Successful machine IRQ entry also consumes it. Failed CPU/DMA steps preserve it.
|
Successful machine IRQ entry also consumes it. Failed CPU/DMA steps preserve it.
|
||||||
|
|
@ -370,8 +379,9 @@ See [the evidence and limitations](../research/dma-resume-timing.md).
|
||||||
|
|
||||||
Important timing limits:
|
Important timing limits:
|
||||||
|
|
||||||
- Source S/N kinds follow instruction-local summaries, not complete neighboring instruction/data-access sequencing.
|
- Source S/N kinds follow the preceding successful instruction; data timing still uses an instruction-local trace.
|
||||||
- Code costs follow actual source and target-pair addresses, but cold current/decode fills have no separate startup charge.
|
- Code costs follow actual source and target-pair addresses, but cold current/decode fills have no separate startup charge.
|
||||||
|
Cold entry, debugger invalidation, and PC/state mismatches use nominal S, not hardware-validated startup timing.
|
||||||
- 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.
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,16 @@ Refill and IRQ fetch-timing research:
|
||||||
See [source-fetch and target-pair timing](research/refill-fetch-timing.md) for the implementation and public-report audit.
|
See [source-fetch and target-pair timing](research/refill-fetch-timing.md) for the implementation and public-report audit.
|
||||||
Source review and comparisons between our own core versions are not physical-hardware timing validation.
|
Source review and comparisons between our own core versions are not physical-hardware timing validation.
|
||||||
|
|
||||||
|
Persistent fetch access-kind research:
|
||||||
|
|
||||||
|
- [ARM7TDMI load-multiple timing](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Load-multiple-registers), for separate prefetch, data-transfer, and internal-cycle phases.
|
||||||
|
- [NanoBoyAdvance ARM handlers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler32.inl) and [Thumb handlers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler16.inl), for next-fetch kinds after data/internal work.
|
||||||
|
- Its [bus timing](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc), for separate internal and ROM costs when prefetch is inactive.
|
||||||
|
- [ares processor memory helpers](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/memory.cpp), [instruction loop](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instruction.cpp), and [ARM handlers](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instructions-arm.cpp), for independent burst termination and fetch-before-execution ordering.
|
||||||
|
|
||||||
|
See [persistent fetch access kinds](research/fetch-access-sequencing.md) for scope, cold-start policy, and regression-report changes.
|
||||||
|
These are source comparisons, not hardware timing measurements or external-emulator test runs.
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ Cold fill and target-pair samples do not add nominal cycles or data accesses.
|
||||||
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only costs with source-fetch and target-pair costs.
|
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only costs with source-fetch and target-pair costs.
|
||||||
IRQ entry also samples its discarded old-state fetch before the vector pair.
|
IRQ entry also samples its discarded old-state fetch before the vector pair.
|
||||||
The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill code cost from the newly sampled 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.
|
The later [sequencing extension](fetch-access-sequencing.md) stores the next fetch kind in the instruction buffer.
|
||||||
|
Cold-fill startup costs and per-access 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.
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ DMA still executes only between instructions, not between data accesses or durin
|
||||||
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only summaries with source-fetch and target-pair costs.
|
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only summaries with source-fetch and target-pair costs.
|
||||||
Resume changes the source access to N; the first target access independently remains N.
|
Resume changes the source access to N; the first target access independently remains N.
|
||||||
No extra access is appended. A resumed ROM branch can therefore cost more than its non-resumed refill.
|
No extra access is appended. A resumed ROM branch can therefore cost more than its non-resumed refill.
|
||||||
Stores and fetches at 128 KiB ROM boundaries already use N and receive no duplicate cost.
|
The later [sequencing extension](fetch-access-sequencing.md) applies each instruction's data/internal effects to the following source fetch.
|
||||||
|
CPU history, DMA resume, and ROM page boundaries can all require N without duplicating an access.
|
||||||
|
A completed transfer after DMA can establish a new N requirement for the following fetch.
|
||||||
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.
|
||||||
|
|
|
||||||
129
docs/research/fetch-access-sequencing.md
Normal file
129
docs/research/fetch-access-sequencing.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Persistent CPU fetch access kinds
|
||||||
|
|
||||||
|
S means sequential access; N means non-sequential access. W is the instruction width.
|
||||||
|
|
||||||
|
## Finding and evidence
|
||||||
|
|
||||||
|
Source-fetch addresses and refill target pairs were already timed separately.
|
||||||
|
However, the current instruction still selected its own source access kind.
|
||||||
|
Stores selected N immediately; loads, register shifts, and multiplies did not affect the following instruction.
|
||||||
|
IRQ entry always selected S unless DMA had completed.
|
||||||
|
This lost the sequence break between instruction execution and the next fetch.
|
||||||
|
|
||||||
|
The [ARM7TDMI load-multiple description](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Load-multiple-registers) separates the initial prefetch, data transfers, and final internal cycle.
|
||||||
|
Instruction cycle totals alone do not identify which neighboring fetch receives the bus-sequence change.
|
||||||
|
Two pinned implementations provide a source-level cross-check:
|
||||||
|
|
||||||
|
- [NanoBoyAdvance ARM handlers at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler32.inl) set the next pipeline access to N after transfers, swaps, register shifts, and multiplies.
|
||||||
|
- Its [Thumb handlers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/handlers/handler16.inl) do the same for corresponding Thumb operations.
|
||||||
|
- Its [CPU loop and refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) consume that kind before execution, including discarded IRQ fetches.
|
||||||
|
Failed ARM conditions set the following access to S. Target refills end with S.
|
||||||
|
- Its [bus timing](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) charges internal cycles and requested ROM wait costs separately when prefetch is inactive.
|
||||||
|
- [ares memory helpers at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/memory.cpp) end a burst on internal cycles and data loads/stores.
|
||||||
|
Its [instruction loop](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instruction.cpp) fetches before execution and restarts target refills with N followed by S.
|
||||||
|
Its [ARM handlers](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/component/processor/arm7tdmi/instructions-arm.cpp) supply the corresponding internal cycles.
|
||||||
|
|
||||||
|
This is agreement between source implementations, not a physical-hardware timing result.
|
||||||
|
No external emulator was executed. No external implementation or ROM was imported into the repository.
|
||||||
|
Game Pak prefetch and within-instruction bus scheduling remain outside this subset.
|
||||||
|
|
||||||
|
## Implemented subset
|
||||||
|
|
||||||
|
The CPU instruction buffer retains the next source-fetch kind alongside its expected PC and instruction state.
|
||||||
|
The current source fetch consumes the incoming kind, with the existing one-shot DMA override.
|
||||||
|
Successful execution then selects the following kind:
|
||||||
|
|
||||||
|
| Completed operation | Following fetch |
|
||||||
|
| --- | --- |
|
||||||
|
| Arithmetic without an internal cycle, immediate shifts, status transfers, skipped conditions, BL prefix | S |
|
||||||
|
| Loads/stores, block/stack transfers, swaps | N |
|
||||||
|
| Register-controlled shifts and multiplies | N |
|
||||||
|
| Branch, PC load/write, SWI, or machine IRQ target refill | S |
|
||||||
|
|
||||||
|
Register-controlled shifts break the sequence even when the shift amount is zero.
|
||||||
|
Data operations break it even when data lies in RAM or I/O rather than ROM.
|
||||||
|
A PC load finishes its data/internal work before its target pair; the pair therefore establishes S for target execution.
|
||||||
|
The target pair itself remains N(T), S(T+W).
|
||||||
|
|
||||||
|
`Cpu::step` and `Cpu::step_timed` commit the same sequence state.
|
||||||
|
Clones and full CPU equality include it. Failed instructions preserve it with the existing instruction buffer.
|
||||||
|
A failed current-slot fetch cannot consume it. Deferred lookahead errors do not prevent a successful current instruction from updating it.
|
||||||
|
IRQ entry captures the incoming kind before exception entry discards the old buffer.
|
||||||
|
Masked IRQ requests leave it unchanged.
|
||||||
|
|
||||||
|
DMA history stays in `Memory`, separate from CPU continuation state.
|
||||||
|
Either source can require N; their combination does not add a second fetch or a fixed surcharge.
|
||||||
|
A completed load after DMA can establish a new N requirement for the following fetch.
|
||||||
|
Host reads/writes, clock advancement, and HALT/STOP idle do not consume the CPU continuation.
|
||||||
|
|
||||||
|
Cold startup still uses nominal S and has no separate current/decode fill charge.
|
||||||
|
Debugger invalidation and PC/state mismatches deliberately return to that cold policy.
|
||||||
|
They cannot reuse another continuation's access kind. Debugger invalidation does not clear Memory's DMA override.
|
||||||
|
This policy is not a claim about hardware reset/startup timing.
|
||||||
|
A status-restoration quirk that changes instruction state without a normal refill also retains the existing cold-buffer policy.
|
||||||
|
|
||||||
|
## Example with default WS0 settings
|
||||||
|
|
||||||
|
For ARM code away from ROM boundaries, S costs 6 cycles and N costs 8 cycles.
|
||||||
|
Consider a RAM word load followed by two ordinary arithmetic instructions:
|
||||||
|
|
||||||
|
1. The load consumes its incoming S source fetch, then performs its data read and internal cycle.
|
||||||
|
2. The first arithmetic instruction consumes N because of the preceding load.
|
||||||
|
3. The second arithmetic instruction consumes S again.
|
||||||
|
|
||||||
|
A store follows the same source-kind sequence, without the load's internal cycle.
|
||||||
|
The store no longer changes its own source fetch retroactively.
|
||||||
|
A WAITCNT store uses old settings for its source fetch, then new settings and N for the following fetch.
|
||||||
|
A branch after a load consumes N at the source and still charges the target N+S pair.
|
||||||
|
|
||||||
|
N is an access kind, not always a positive cycle penalty.
|
||||||
|
Some wait settings make N cheaper than S. Tests use independent N/S tables rather than adding a fixed cost.
|
||||||
|
ROM page boundaries can force N independently; a forced boundary and a retained N do not duplicate an access.
|
||||||
|
|
||||||
|
## Original regression coverage
|
||||||
|
|
||||||
|
Instruction-pair tests cover:
|
||||||
|
|
||||||
|
- Supported ARM/Thumb transfer families, block/stack operations, signed loads, swaps, shifts, multiplies, and status operations.
|
||||||
|
- Skipped conditions, zero shift amounts, BL prefixes, and ordinary arithmetic.
|
||||||
|
- Every ROM window and wait setting, both instruction widths, and stored prefetch-bit values.
|
||||||
|
- Repeated transfers, branches after loads, and PC-load refills that restore S.
|
||||||
|
- Timed/untimed equivalence, cloning, failed data accesses, missing current slots, and masked IRQ requests.
|
||||||
|
- DMA and CPU sequence-break composition, IRQ source timing, and handler refill state.
|
||||||
|
- Host and HALT isolation, debugger invalidation, PC mismatches, and page/wait-window crossings.
|
||||||
|
|
||||||
|
Existing rollback, instruction semantics, load data/internal costs, and deferred diagnostics remain unchanged.
|
||||||
|
WAITCNT and DMA tests now distinguish a consumed resume from a new sequence break caused by the completed instruction.
|
||||||
|
|
||||||
|
## Validation and report audit
|
||||||
|
|
||||||
|
Workspace and core/demo tests pass in debug and release on Darwin arm64.
|
||||||
|
Formatting, clippy, rustdoc, Python preparation tests, native ROM windows, and all graphics smoke modes pass.
|
||||||
|
The release executable is native Mach-O arm64.
|
||||||
|
Public ARM, Thumb, memory, and BIOS checkpoint assertions pass; debug and release reports match.
|
||||||
|
|
||||||
|
Compared with the preceding refill-timing change:
|
||||||
|
|
||||||
|
- ARM nominal cycles increase by 208; Thumb increases by 106; memory increases by 32.
|
||||||
|
Their other report fields remain unchanged.
|
||||||
|
- BIOS executes 963 fewer instructions and steps, with the same final nominal cycle total and all other report fields unchanged.
|
||||||
|
|
||||||
|
An original temporary trace runner compared both core versions against the pinned BIOS fixture.
|
||||||
|
The VBlank polling loop now costs 36 rather than 34 cycles because its load makes the following test instruction fetch N.
|
||||||
|
The updated core executes 321 fewer three-instruction polling loops.
|
||||||
|
IRQ entry moves to another position in the loop; the final checkpoint still has r12=0 and one IRQ entry.
|
||||||
|
This is an audit between our core versions, not external-emulator or hardware timing validation.
|
||||||
|
|
||||||
|
The original timer demo keeps its IRQ step and total duration.
|
||||||
|
Moving store fetch costs to following instructions shifts timer start two cycles earlier.
|
||||||
|
The timer therefore runs two additional cycles before the handler stops it.
|
||||||
|
|
||||||
|
## Remaining limits
|
||||||
|
|
||||||
|
- Cold-start costs and state-changing test/compare quirks are not hardware-validated.
|
||||||
|
- Data timing still uses an instruction-local access trace. This is not a complete bus-event scheduler.
|
||||||
|
- Device clocks still update once per instruction, IRQ entry, or DMA unit.
|
||||||
|
- Game Pak prefetch, configurable EWRAM timing, display contention, and DMA during internal cycles remain unimplemented.
|
||||||
|
- IRQ synchronization delays and complete BIOS/general-bus latch history remain incomplete.
|
||||||
|
|
||||||
|
The next timing work is Game Pak prefetch, with explicit startup and invalidation rules and independent tests.
|
||||||
|
|
@ -27,7 +27,8 @@ All programs are original; no external source code or ROM was imported.
|
||||||
|
|
||||||
`Cpu::step_timed` captures the address of `Fetched.lookahead` before execution.
|
`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.
|
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.
|
The later [sequencing extension](fetch-access-sequencing.md) retains the incoming access kind from the preceding instruction.
|
||||||
|
The pending DMA resume still overrides that source access to N.
|
||||||
WAITCNT is captured before CPU execution, so a CPU store cannot retroactively change its own fetch cost.
|
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.
|
The same rule applies to cold and retained pipelines.
|
||||||
|
|
@ -65,8 +66,8 @@ Timer tests and the original timer demo now reflect this bounded rule.
|
||||||
|
|
||||||
The later [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs for branches and IRQ entry.
|
The later [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs for branches and IRQ entry.
|
||||||
|
|
||||||
- Instruction-local S/N summaries remain in use, including the existing store rule.
|
- Source S/N kinds now follow the preceding instruction's data/internal work.
|
||||||
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
|
Data timing and device scheduling still do not form a complete chronological bus trace.
|
||||||
- Refills now charge one source fetch plus the target N+S pair, not an additional fetch beside the old destination-only summary.
|
- Refills now charge one source fetch plus the target N+S pair, not an additional fetch beside the old destination-only summary.
|
||||||
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
|
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
|
||||||
- Cold fills have no separate startup charge, even though their samples update supported bus history.
|
- Cold fills have no separate startup charge, even though their samples update supported bus history.
|
||||||
|
|
@ -74,7 +75,7 @@ The later [refill timing extension](refill-fetch-timing.md) also charges source
|
||||||
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
- 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.
|
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
|
||||||
The next timing step is verified access-kind sequencing between instructions and data accesses.
|
The next timing step is Game Pak prefetch with verified startup and invalidation rules.
|
||||||
|
|
||||||
## Original regression coverage
|
## Original regression coverage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ A retry takes a new lookahead sample from current memory.
|
||||||
|
|
||||||
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
|
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
|
||||||
The [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs individually.
|
The [refill timing extension](refill-fetch-timing.md) also charges source fetches and target pairs individually.
|
||||||
Instruction-local access kinds and cold-start costs remain nominal.
|
The later [sequencing extension](fetch-access-sequencing.md) retains source kinds across CPU data/internal work.
|
||||||
|
Cold-start costs remain 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ For an instruction executing at P, `Cpu::step_timed` charges its sampled lookahe
|
||||||
|
|
||||||
- ARM source: P+8, word width.
|
- ARM source: P+8, word width.
|
||||||
- Thumb source: P+4, halfword width.
|
- Thumb source: P+4, halfword width.
|
||||||
- Access kind: the existing instruction-local summary, overridden to N after successful DMA.
|
- Access kind: now retained from the preceding instruction by the [sequencing extension](fetch-access-sequencing.md), with DMA override.
|
||||||
- WAITCNT: captured before instruction effects.
|
- WAITCNT: captured before instruction effects.
|
||||||
|
|
||||||
If the instruction refills, timing also charges N(T) and S(T+W) using the resulting state.
|
If the instruction refills, timing also charges N(T) and S(T+W) using the resulting state.
|
||||||
|
|
@ -112,11 +112,11 @@ The stopped counter therefore increases by five even though the final machine-cy
|
||||||
|
|
||||||
## Remaining limits
|
## Remaining limits
|
||||||
|
|
||||||
- Source access kinds still use instruction-local summaries, including the existing store rule.
|
- The later [sequencing extension](fetch-access-sequencing.md) retains the next source kind after CPU data/internal work.
|
||||||
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
|
Data timing and device scheduling still do not form a complete chronological bus trace.
|
||||||
- Cold current/decode fills have no separate startup timing charge.
|
- Cold current/decode fills have no separate startup timing charge.
|
||||||
- BIOS retained history is not a complete refill/data-access bus latch.
|
- BIOS retained history is not a complete refill/data-access bus latch.
|
||||||
- Missing instruction bytes remain diagnostics, not hardware prefetch aborts.
|
- Missing instruction bytes remain diagnostics, not hardware prefetch aborts.
|
||||||
- Game Pak prefetch, per-access device updates, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
- Game Pak prefetch, per-access device updates, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
||||||
|
|
||||||
The next timing work is independently verified access-kind sequencing between instructions and data accesses.
|
The next timing work is Game Pak prefetch with verified startup and invalidation rules.
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ Strict instruction reads never use protected BIOS or unused-memory data fallback
|
||||||
- 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.
|
||||||
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
|
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
|
||||||
- Refills now charge the source fetch and target pair; instruction-local source access kinds remain nominal.
|
- Refills now charge the source fetch and target pair; the [sequencing extension](fetch-access-sequencing.md) retains incoming source kinds.
|
||||||
- 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
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ CPU stores, DMA transfers, and host writes never replace retained instructions a
|
||||||
`Cpu::invalidate_pipeline()` explicitly discards retained instructions for debugger repair or attachment to different memory.
|
`Cpu::invalidate_pipeline()` explicitly discards retained instructions for debugger repair or attachment to different memory.
|
||||||
It does not clear separate BIOS or IWRAM bus history.
|
It does not clear separate BIOS or IWRAM bus history.
|
||||||
CPU clones own independent buffers; full CPU equality includes them.
|
CPU clones own independent buffers; full CPU equality includes them.
|
||||||
|
The later [sequencing extension](fetch-access-sequencing.md) also retains the next fetch kind in each buffer.
|
||||||
|
|
||||||
## Separation from bus history and timing
|
## Separation from bus history and timing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
||||||
- Code costs use actual ARM PC+8/Thumb PC+4 source fetches and branch/exception target pairs, including region and ROM boundaries.
|
- Code costs use actual ARM PC+8/Thumb PC+4 source fetches and branch/exception target pairs, including region and ROM boundaries.
|
||||||
- IRQ entry samples its discarded incoming-state fetch before the ARM vector pair, with local IWRAM history updates.
|
- IRQ entry samples its discarded incoming-state fetch before the ARM vector pair, with local IWRAM history updates.
|
||||||
|
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
||||||
- 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.
|
||||||
|
|
@ -114,6 +115,7 @@ Block transfers validate all accesses before committing register, RAM, or I/O ch
|
||||||
This diagnostic policy does not reproduce partial transfers during hardware data aborts.
|
This diagnostic policy does not reproduce partial transfers during hardware data aborts.
|
||||||
CPU instructions, including swaps, finish before a newly requested DMA unit runs. Sub-instruction bus arbitration is not modeled.
|
CPU instructions, including swaps, finish before a newly requested DMA unit runs. Sub-instruction bus arbitration is not modeled.
|
||||||
CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
||||||
|
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
||||||
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
||||||
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
||||||
Horizontal window comparators retain four-cycle event history.
|
Horizontal window comparators retain four-cycle event history.
|
||||||
|
|
@ -164,8 +166,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. Extend instruction-local source access kinds into verified neighboring instruction/data-access sequencing.
|
2. Add Game Pak prefetch with verified startup and invalidation rules.
|
||||||
Add startup costs, Game Pak prefetch, per-access device updates, and timer/IRQ delays.
|
Validate cold-start costs, then extend per-access device updates, bus arbitration, 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.
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,8 @@ Timed/untimed tests verify CPU results and nominal clock costs.
|
||||||
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
||||||
ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes.
|
ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes.
|
||||||
They cover refill target pairs, PC writes to fallthrough, conditional branches, ARM/Thumb transitions, exception entry/return, and deferred fetch errors.
|
They cover refill target pairs, PC writes to fallthrough, conditional branches, ARM/Thumb transitions, exception entry/return, and deferred fetch errors.
|
||||||
Full CPU equality checks buffer rollback and cloning. Successful instruction-semantic tests compare architectural state separately.
|
Full CPU equality checks buffer and next-fetch-kind rollback and cloning.
|
||||||
|
Successful instruction-semantic tests compare architectural state separately.
|
||||||
Explicit invalidation tests cover debugger repair. Timed/untimed tests verify equal buffers without added nominal cycles.
|
Explicit invalidation tests cover debugger repair. Timed/untimed tests verify equal buffers without added nominal cycles.
|
||||||
The display-runner error test allows an already buffered branch to complete before the patched unsupported word executes.
|
The display-runner error test allows an already buffered branch to complete before the patched unsupported word executes.
|
||||||
Thumb instruction-buffer tests verify retained P+2/P+4 halfwords, BL prefix/suffix behavior, and state-tagged target pairs.
|
Thumb instruction-buffer tests verify retained P+2/P+4 halfwords, BL prefix/suffix behavior, and state-tagged target pairs.
|
||||||
|
|
@ -259,6 +260,11 @@ Refill timing tests cover incoming/outgoing widths, all ROM wait settings, and t
|
||||||
They verify source DMA overrides, split wait-window pairs, missing source/target diagnostics, masked IRQ isolation, and device totals.
|
They verify source DMA overrides, split wait-window pairs, missing source/target diagnostics, masked IRQ isolation, and device totals.
|
||||||
IRQ history tests verify word/halfword lane updates without invented cold fills or BIOS instruction snapshots.
|
IRQ history tests verify word/halfword lane updates without invented cold fills or BIOS instruction snapshots.
|
||||||
See [refill timing](research/refill-fetch-timing.md) for source evidence and public-report changes.
|
See [refill timing](research/refill-fetch-timing.md) for source evidence and public-report changes.
|
||||||
|
Instruction-pair tests cover following-fetch kinds after transfers, swaps, register shifts, multiplies, status operations, and skipped conditions.
|
||||||
|
They use independent N/S tables across every ROM window and wait setting, including settings where N is cheaper than S.
|
||||||
|
They check timed/untimed equivalence, clones, IRQ/DMA composition, repeated transfers, PC-load refills, failed steps, and cold-start policy.
|
||||||
|
Page/wait-window crossings, masked IRQs, host/HALT isolation, and pre-write WAITCNT settings retain separate checks.
|
||||||
|
See [persistent fetch access kinds](research/fetch-access-sequencing.md) for evidence and public-report changes.
|
||||||
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue