feat(cpu): retain fetch access kinds across instruction execution

This commit is contained in:
0xrsydn 2026-10-06 16:53:05 +07:00
commit 86321f0071
21 changed files with 637 additions and 61 deletions

View file

@ -42,6 +42,8 @@ mod fetch_boundary_tests;
#[cfg(test)]
mod fetch_history_tests;
#[cfg(test)]
mod fetch_sequence_tests;
#[cfg(test)]
mod fetch_timing_tests;
#[cfg(test)]
mod instruction_tests;
@ -176,7 +178,7 @@ impl InstructionSet {
}
/// 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)]
pub struct Cpu {
registers: [u32; 16],
@ -242,7 +244,7 @@ impl Cpu {
) -> Result<(), CpuError> {
let pipeline::Fetched {
instruction,
continuation,
mut continuation,
lookahead,
fill,
} = fetched;
@ -250,14 +252,15 @@ impl Cpu {
// Always clear the access context, including on diagnostic errors.
let pc = self.pc();
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);
let result = self.execute_instruction(instruction, memory);
memory.end_cpu_access(result.is_ok());
if result.is_ok() {
if refill {
if summary.refill {
self.refill_pipeline(memory);
} else {
continuation.next_access = summary.next_fetch_kind();
// A state change without an ordinary refill starts a cold sequence.
self.pipeline = if self.instruction_set == instruction_set
&& self.pc() == pc.wrapping_add(instruction_set.width())

View file

@ -165,6 +165,8 @@ fn failed_cpu_fetch_and_data_access_preserve_resume_until_success() {
assert_eq!(timing.code_cycles, 8);
assert_eq!(timing.data_cycles, 6);
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);
}
@ -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() {
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.
(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.
] {
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();
assert_eq!(timing.code_cycles, 8);
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);
}
}
@ -210,6 +216,7 @@ fn waitcnt_cpu_store_uses_old_code_cost_but_dma_store_applies_before_resume() {
cpu.registers[1] = WAITCNT;
dma(&mut bus);
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);
let (mut cpu, mut bus) = program(false, PC, 0xe1a0_0000);

View 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);
}
}
}

View file

@ -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[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.code_cycles, 10); // Old WS1 S+S=10 at lookahead 0x0a000004.
assert_eq!(cost.data_cycles, 1);
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);
}

View file

@ -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 crate::memory::{InstructionFetch, Memory, MemoryError};
use crate::{
memory::{InstructionFetch, Memory, MemoryError},
timing::AccessKind,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Pipeline {
@ -9,6 +12,7 @@ pub(super) struct Pipeline {
// Deferred strict-fetch results for the next execute and decode positions.
// Thumb halfwords are zero-extended; their upper bits are not another opcode.
instructions: [Result<u32, MemoryError>; 2],
pub(super) next_access: AccessKind,
}
pub(super) struct Fetched {
@ -21,11 +25,21 @@ pub(super) struct Fetched {
impl Cpu {
/// Discard buffered instructions after debugger code edits or rebinding memory.
/// 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) {
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> {
let pc = self.pc();
let instruction_set = self.instruction_set;
@ -53,6 +67,7 @@ impl Cpu {
pc: pc.wrapping_add(width),
instruction_set,
instructions: [decode, fetched.instruction.clone()],
next_access: AccessKind::Sequential, // Set from execution only on success.
},
lookahead: fetched,
fill,
@ -73,6 +88,7 @@ impl Cpu {
pc,
instruction_set,
instructions: fetches.map(|fetch| fetch.instruction),
next_access: AccessKind::Sequential,
});
}
}

View file

@ -1,6 +1,6 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! 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 crate::{
@ -9,13 +9,23 @@ use crate::{
};
#[derive(Default)]
struct Summary {
code_kind: AccessKind,
refill: bool,
pub(super) struct Summary {
data: bool,
pub(super) refill: bool,
internal: u32,
}
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 {
Self {
refill: true,
@ -25,11 +35,7 @@ impl Summary {
fn transfer(load: bool, pc: bool) -> Self {
Self {
code_kind: if load {
AccessKind::Sequential
} else {
AccessKind::NonSequential
},
data: true,
refill: load && pc,
internal: u32::from(load),
}
@ -52,15 +58,12 @@ impl Cpu {
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
let fetched = self.fetch(memory)?;
let instruction = fetched.instruction;
let summary = match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction),
};
let summary = self.instruction_summary(instruction);
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();
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();
let result = self.execute_fetched(fetched, memory);
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> {
let state = self.instruction_set;
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) {
return None;
}
let waitcnt = memory.waitcnt();
let kind = memory.cpu_code_kind(AccessKind::Sequential);
let fetch = memory.fetch_instruction(address, state);
memory.discarded_cpu_fetch(&fetch);
self.refill_pipeline(memory);
@ -122,6 +125,7 @@ impl Cpu {
}
if instruction & 0x0fb0_0ff0 == 0x0100_0090 {
return Summary {
data: true,
internal: 1,
..Summary::default()
}; // 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.
pub(super) fn instruction_refills(&self, instruction: u32) -> bool {
pub(super) fn instruction_summary(&self, instruction: u32) -> Summary {
match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction).refill,
InstructionSet::Thumb => self.thumb_summary(instruction).refill,
InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction),
}
}

View file

@ -338,9 +338,9 @@ fn waitcnt_store_charges_old_code_settings_and_new_settings_apply_next_step() {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = WAITCNT;
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!(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]

View file

@ -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.memory().read16(IF).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.
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff9);
// Persistent fetch kinds move costs between setup instructions and the running timer.
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfffb);
for _ in 0..100 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}

View file

@ -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/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/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/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/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/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 |

View file

@ -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 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.
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.
This is instruction buffering, not a complete timed fetch pipeline.
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.
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),
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.
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.
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.
Instruction buffering is discarded on a state change without an ordinary refill.
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
The timing model uses ARM7 cycle summaries:
The timing model distinguishes these ARM7 cycle types:
- **S:** sequential memory access.
- **N:** non-sequential memory access.
- **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.
Loads cost a code S access, a data N access, and 1I. Stores use N for code and data.
Block transfers charge N for the first data word and S for each following word.
Loads add 1I. Loading PC also adds the branch refill cost.
Every instruction charges its source fetch using the incoming access kind retained by the CPU buffer.
Ordinary arithmetic and immediate shifts leave the next fetch S.
Register-specified shifts add 1I, even for a zero shift amount, and leave the next fetch N.
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.
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.
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.
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.
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.
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) 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.
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.
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.
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 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:
- 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.
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.
- 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.

View file

@ -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.
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.
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.

View file

@ -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.
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.
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.
The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads.

View file

@ -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.
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.
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.
This change does not select a DMA value for CPU open-bus reads.

View 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.

View file

@ -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.
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.
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.
- 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.
- Source S/N kinds now follow the preceding instruction's data/internal work.
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.
- 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.
@ -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.
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

View file

@ -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 [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.
No boundary timing accuracy is claimed.

View file

@ -36,7 +36,7 @@ For an instruction executing at P, `Cpu::step_timed` charges its sampled lookahe
- ARM source: P+8, word 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.
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
- Source access kinds still use instruction-local summaries, including the existing store rule.
Neighboring instruction/data-access sequencing is not a complete chronological bus trace.
- The later [sequencing extension](fetch-access-sequencing.md) retains the next source kind after CPU data/internal work.
Data timing and device scheduling still do not form a complete chronological bus trace.
- Cold current/decode fills have no separate startup timing charge.
- BIOS retained history is not a complete refill/data-access bus latch.
- 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.
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.

View file

@ -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.
- 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).
- 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.
## Original tests

View file

@ -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.
It does not clear separate BIOS or IWRAM bus history.
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

View file

@ -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.
- 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.
- 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.
- 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.
@ -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.
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.
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.
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
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.
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. Extend instruction-local source access kinds into verified neighboring instruction/data-access sequencing.
Add startup costs, Game Pak prefetch, per-access device updates, and timer/IRQ delays.
2. Add Game Pak prefetch with verified startup and invalidation rules.
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.
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.

View file

@ -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.
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.
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.
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.
@ -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.
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.
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.
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.