feat(cpu): account for source fetches in branch and IRQ refills

This commit is contained in:
0xrsydn 2026-10-06 16:27:45 +07:00
commit 2d7eb6e4eb
27 changed files with 638 additions and 87 deletions

View file

@ -56,6 +56,8 @@ mod load_alias_tests;
#[cfg(test)]
mod open_bus_tests;
#[cfg(test)]
mod refill_timing_tests;
#[cfg(test)]
mod status_tests;
#[cfg(test)]
mod tests;

View file

@ -235,7 +235,7 @@ fn ram_instruction_consumes_resume_instead_of_delaying_it_until_rom_execution()
fn branch_refill_and_irq_entry_consume_resume_without_an_extra_nominal_access() {
let (mut cpu, mut bus) = program(false, PC, 0xea00_0000); // B PC+8.
dma(&mut bus);
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 20); // Existing N+2S summary.
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 22); // Source N=8 + target N=8, S=6.
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
let (cpu, _) = program(false, PC, 0xe1a0_0000);
@ -249,14 +249,15 @@ fn branch_refill_and_irq_entry_consume_resume_without_an_extra_nominal_access()
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.last_timing().code_cycles, 3);
// Source N=8 + BIOS pair 2.
assert_eq!(machine.last_timing().code_cycles, 10);
// Inspect the bus through a separate CPU after IRQ entry, before a handler runs.
let mut probe = Cpu::new(PC);
assert_eq!(
probe.step_timed(machine.memory_mut()).unwrap().code_cycles,
6
);
assert_eq!(machine.memory().cycles(), 12);
assert_eq!(machine.memory().cycles(), 19); // DMA 9 + IRQ entry 10.
}
#[test]

View file

@ -1,4 +1,4 @@
//! Original fetch-address timing tests. Access kinds/refills remain nominal summaries.
//! Original fetch-address timing tests. Source access kinds remain nominal summaries.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
@ -189,7 +189,7 @@ fn unavailable_lookahead_keeps_nominal_address_cost_and_fails_only_when_executed
}
#[test]
fn refill_summaries_do_not_add_the_new_lookahead_cost_a_second_time() {
fn refills_charge_one_source_fetch_and_two_target_fetches() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 32);
@ -199,7 +199,10 @@ fn refill_summaries_do_not_add_the_new_lookahead_cost_a_second_time() {
memory.write32(cpu.pc(), 0xe12f_ff11).unwrap();
}
cpu.registers[1] = DEST | u32::from(thumb);
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 3);
assert_eq!(
cpu.step_timed(&mut memory).unwrap().code_cycles,
if thumb { 3 + 2 } else { 6 + 2 }
);
}
}

View file

@ -390,7 +390,7 @@ fn successful_dma_between_refill_and_target_execution_updates_refill_history() {
}
#[test]
fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() {
fn refill_snapshots_keep_timed_untimed_results_and_charge_source_plus_target_pair() {
for thumb in [false, true] {
for destination in [TARGET, TARGET + 2] {
let (cpu, mut bus) = branch(ROM_START, thumb, destination);
@ -403,13 +403,22 @@ fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() {
assert_eq!(machine.cpu(), &untimed);
assert_eq!(
machine.last_timing().code_cycles,
if index == 0 { 3 } else { 1 }
if index == 0 {
if thumb {
5
} else {
8
}
} else {
1
}
);
assert_eq!(machine.last_timing().data_cycles, index);
assert_eq!(machine.last_timing().internal_cycles, index);
}
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6);
let cycles = if thumb { 8 } else { 11 }; // Source fetch, target pair, then 1S+1N+1I load.
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), cycles as u16);
assert_eq!(bus.cycles(), 0);
}
}

View file

@ -0,0 +1,279 @@
//! Original tests with independent source-fetch and target-pair cost tables.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
memory::{BIOS_SIZE, ROM_START},
timing::StepTiming,
};
const RAM: u32 = 0x0300_0100;
const ARM_NOP: u32 = 0xe1a0_0000;
fn bx(thumb: bool, pc: u32, target: u32, memory: &mut Memory) -> Cpu {
let mut cpu = Cpu::new(pc);
cpu.registers[0] = target;
if thumb {
cpu.instruction_set = InstructionSet::Thumb;
memory.write16(pc, 0x4700).unwrap();
} else {
memory.write32(pc, 0xe12f_ff10).unwrap();
}
cpu
}
fn pending_irq(memory: &mut Memory) {
memory.write16(IE, 8).unwrap();
memory.write16(IME, 1).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
memory.advance_cycles(1);
memory.write16(TIMER_BASE + 2, 0).unwrap();
}
fn resume(memory: &mut Memory) {
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, 0x0200_0000).unwrap();
memory.write32(dma + 4, 0x0300_1000).unwrap();
memory.write32(dma + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
}
#[test]
fn bx_target_pairs_use_outgoing_width_and_charge_the_third_fetch_only_on_execution() {
for source_thumb in [false, true] {
for target_thumb in [false, true] {
let width = if target_thumb { 2 } else { 4 };
for window in 0..3 {
let boundary = ROM_START + window as u32 * 0x0200_0000 + 0x20000;
// Boundary in first, second, or third target slot, or no boundary.
for slot in 0..4 {
let target = boundary - slot * width;
let pattern = if target_thumb { 0x46c0_46c0 } else { ARM_NOP };
let mut memory =
Memory::new(pattern.to_le_bytes().repeat(0x20020 / 4)).unwrap();
let cpu = bx(
source_thumb,
RAM,
target | u32::from(target_thumb),
&mut memory,
);
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_half = if target_thumb { 0 } else { s };
let mut cpu = cpu.clone();
let target_pair =
n + extra_half + if slot == 1 { n } else { s } + extra_half;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
StepTiming {
code_cycles: 1 + target_pair,
..StepTiming::default()
}
);
assert_eq!(cpu.pc(), target);
assert_eq!(
cpu.step_timed(&mut memory).unwrap().code_cycles,
if slot == 2 {
n + extra_half
} else {
s + extra_half
}
);
assert_eq!(memory.cycles(), 0);
}
}
}
}
}
}
}
}
#[test]
fn rom_source_fetch_uses_incoming_width_boundary_and_dma_override_before_ram_refill() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for window in 0..3 {
for boundary in [false, true] {
for resumed in [false, true] {
let pc = ROM_START
+ window as u32 * 0x0200_0000
+ if boundary { 0x20000 - 2 * width } else { 0x100 };
let mut rom = vec![0; 0x20020];
let instruction: u32 = if thumb { 0x4700 } else { 0xe12f_ff10 };
let offset = (pc & 0x01ff_ffff) as usize;
rom[offset..offset + width as usize]
.copy_from_slice(&instruction.to_le_bytes()[..width as usize]);
let mut memory = Memory::new(rom).unwrap();
memory.write32(RAM, ARM_NOP).unwrap();
memory.write32(RAM + 4, ARM_NOP).unwrap();
for first in 0..4 {
for second in 0..2 {
memory
.write16(
WAITCNT,
(first << [2, 5, 8][window]) | (second << [4, 7, 10][window]),
)
.unwrap();
let mut cpu = Cpu::new(pc);
if thumb {
cpu.instruction_set = InstructionSet::Thumb;
}
cpu.registers[0] = RAM;
if resumed {
resume(&mut memory);
}
let n = [5, 4, 3, 9][first as usize];
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
let source =
if boundary || resumed { n } else { s } + if thumb { 0 } else { s };
assert_eq!(
cpu.step_timed(&mut memory).unwrap().code_cycles,
source + 2
);
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 1);
// Resume is consumed by the branch, not delayed until another ROM fetch.
let mut probe = Cpu::new(pc);
if thumb {
probe.instruction_set = InstructionSet::Thumb;
}
probe.registers[0] = RAM;
assert_eq!(
probe.step_timed(&mut memory).unwrap().code_cycles,
if boundary { n } else { s } + if thumb { 0 } else { s } + 2
);
}
}
}
}
}
}
}
#[test]
fn irq_charges_discarded_incoming_fetch_even_when_rom_is_missing() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for (pc, normal, resumed) in [
(
0x0200_0100,
if thumb { 3 } else { 6 },
if thumb { 3 } else { 6 },
),
(RAM, 1, 1),
(
ROM_START - width,
if thumb { 3 } else { 6 },
if thumb { 5 } else { 8 },
),
(
ROM_START + 0x100,
if thumb { 3 } else { 6 },
if thumb { 5 } else { 8 },
),
(
ROM_START + 0x20000 - 2 * width,
if thumb { 5 } else { 8 },
if thumb { 5 } else { 8 },
),
(
0x0c00_0000 - 2 * width,
if thumb { 5 } else { 14 },
if thumb { 5 } else { 14 },
),
] {
for dma in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x1c].copy_from_slice(&ARM_NOP.to_le_bytes());
let mut memory = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = Cpu::new(pc);
if thumb {
cpu.instruction_set = InstructionSet::Thumb;
}
let cpsr = cpu.cpsr();
if dma {
resume(&mut memory);
}
pending_irq(&mut memory);
let mut machine = Machine::new(cpu, memory);
let start = machine.cycles();
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
let expected = if dma { resumed } else { normal } + 2;
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: expected,
..StepTiming::default()
}
);
assert_eq!(machine.cycles() - start, u64::from(expected));
assert_eq!(machine.cpu().pc(), 0x18);
assert_eq!(machine.cpu().registers()[14], pc + 4);
assert_eq!(machine.cpu().spsr(), Some(cpsr));
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Arm);
// No old-PC opcode executes, even when its ROM bytes are missing.
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.last_timing().code_cycles, 1);
}
}
}
}
#[test]
fn target_pair_crossing_a_wait_window_uses_each_fetch_region() {
for thumb in [false, true] {
let width = if thumb { 2 } else { 4 };
for (boundary, pair) in [
(0x0a00_0000, if thumb { 10 } else { 18 }),
(0x0c00_0000, if thumb { 10 } else { 24 }),
] {
let mut memory = Memory::new(vec![]).unwrap();
let mut cpu = bx(
false,
RAM,
(boundary - width) | u32::from(thumb),
&mut memory,
);
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 1 + pair);
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err()); // Missing target deferred until current.
assert_eq!(cpu, before);
assert_eq!(memory.cycles(), 0);
}
}
}
#[test]
fn masked_irq_does_not_sample_or_change_cpu_or_consume_dma_resume() {
for cpu_mask in [false, true] {
let mut memory = Memory::new(vec![]).unwrap();
pending_irq(&mut memory);
let mut cpu = Cpu::new(ROM_START);
cpu.irq_disabled = cpu_mask;
if !cpu_mask {
memory.write16(IME, 0).unwrap();
}
resume(&mut memory);
let before = cpu.clone();
let cycles = memory.cycles();
assert_eq!(cpu.take_irq_timed(&mut memory), None);
assert_eq!(cpu, before);
assert_eq!(memory.cycles(), cycles);
assert_eq!(
memory.cpu_code_kind(crate::timing::AccessKind::Sequential),
crate::timing::AccessKind::NonSequential
);
}
}

View file

@ -1,6 +1,6 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Non-refill code costs use the new fetch address; data costs use actual bus accesses.
//! Access kinds and PC-write refill totals remain instruction-local nominal summaries.
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
//! Source access kinds remain instruction-local nominal summaries.
use super::{Cpu, CpuError, InstructionSet};
use crate::{
@ -65,11 +65,10 @@ impl Cpu {
let result = self.execute_fetched(fetched, memory);
let data_cycles = memory.end_data_timing();
result?;
let code_cycles = if summary.refill {
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
} else {
bus_cycles(waitcnt, fetch_address, width, code_kind)
};
let mut code_cycles = bus_cycles(waitcnt, fetch_address, width, code_kind);
if summary.refill {
code_cycles += refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width());
}
Ok(StepTiming {
code_cycles,
data_cycles,
@ -78,16 +77,24 @@ impl Cpu {
})
}
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
StepTiming {
code_cycles: refill_cycles(
memory.waitcnt(),
self.pc(),
self.instruction_set.access_width(),
),
..StepTiming::default()
/// Accept IRQ between instructions, discard the old-state fetch, then refill ARM.
/// A missing discarded fetch cannot prevent entry or become a current-slot error.
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());
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);
Some(StepTiming {
code_cycles: bus_cycles(waitcnt, fetch.address(), state.access_width(), kind)
+ refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width()),
..StepTiming::default()
})
}
fn arm_summary(&self, instruction: u32) -> Summary {
@ -198,7 +205,7 @@ impl Cpu {
Summary::branch()
}
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => Summary::branch(),
// Thumb BL prefix is 1S in the old region; its suffix is 2S+1N at the target.
// Thumb BL prefix fetches once; its suffix adds the target N+S pair.
_ => Summary::default(),
}
}

View file

@ -78,14 +78,14 @@ fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
}
#[test]
fn cross_region_bx_uses_destination_width_and_wait_states() {
fn cross_region_bx_uses_source_and_destination_widths_and_wait_states() {
let (mut cpu, mut memory) = arm(0xe12f_ff10);
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
cpu.registers[0] = 0x0a00_0101; // IWRAM source 1 + Thumb WS1 target pair 5N + 5S.
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(11, 0, 0));
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
let (mut cpu, mut memory) = thumb(0x4700);
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
cpu.registers[0] = ROM_START + 0x100; // IWRAM source 1 + ARM WS0 target pair 8N + 6S.
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
}
#[test]
@ -130,7 +130,7 @@ fn load_pc_adds_refill_and_does_not_count_fetch_as_data() {
let (mut cpu, mut memory) = arm(0xe590_f000);
cpu.registers[0] = DATA;
memory.write32(DATA, ROM_START + 0x100).unwrap();
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 6, 1));
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 6, 1));
}
#[test]
@ -321,7 +321,7 @@ fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
cpu.registers[14] = ROM_START + 0x100;
assert_eq!(
cpu.step_timed(&mut memory).unwrap(),
expected(if thumb_state { 11 } else { 20 }, 0, 0)
expected(if thumb_state { 9 } else { 15 }, 0, 0)
);
}
}

View file

@ -177,9 +177,8 @@ impl Machine {
self.last_timing = timing;
return Ok(StepKind::HaltIdle);
}
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
self.cpu.refill_pipeline(&mut self.memory);
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
let (kind, timing) = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
(StepKind::IrqEntry, timing)
} else {
(
StepKind::Instruction,

View file

@ -3,6 +3,8 @@ use std::{cell::Cell, error::Error, fmt};
mod fetch;
#[cfg(test)]
mod fetch_tests;
#[cfg(test)]
mod irq_fetch_tests;
mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
@ -432,6 +434,14 @@ impl Memory {
}
}
/// IRQ's discarded source fetch drives only its actual IWRAM lanes.
/// Do not invent cold fills or commit a BIOS instruction snapshot: none executed.
pub(crate) fn discarded_cpu_fetch(&mut self, fetch: &InstructionFetch) {
self.iwram_bus.begin();
self.drive_iwram_fetch(fetch);
self.iwram_bus.finish(true);
}
/// Successful IWRAM target fetches drive their lanes in order, in either CPU state.
/// Other-region and unavailable target fetches leave the local latch unchanged.
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {

View file

@ -0,0 +1,90 @@
//! IRQ fetch history is distinct from executing a BIOS instruction.
use super::*;
use crate::{
cpu::Cpu,
dma::DMA_STRIDE,
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
};
const RAM: u32 = 0x0300_0100;
const SEED: u32 = 0xa1b2_c3d4;
fn seeded_irq(memory: &mut Memory) {
// DMA supplies known lanes after CPU setup, before the IRQ's discarded fetch.
memory.write32(0x0200_1000, SEED).unwrap();
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, 0x0200_1000).unwrap();
memory.write32(dma + 4, RAM + 0x100).unwrap();
memory.write32(dma + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
memory.write16(IE, 8).unwrap();
memory.write16(IME, 1).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
memory.advance_cycles(1);
memory.write16(TIMER_BASE + 2, 0).unwrap();
}
#[test]
fn irq_drives_only_the_discarded_iwram_fetch_in_incoming_state() {
for thumb in [false, true] {
for offset in [0, 2] {
if !thumb && offset != 0 {
continue;
}
let pc = RAM + offset;
let mut memory = Memory::new(vec![]).unwrap();
let cpu = if thumb {
// Original EWRAM bootstrap: load Thumb target then BX.
memory.write32(0x0200_0000, 0xe59f_0000).unwrap();
memory.write32(0x0200_0004, 0xe12f_ff10).unwrap();
memory.write32(0x0200_0008, pc | 1).unwrap();
let mut cpu = Cpu::new(0x0200_0000);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
cpu
} else {
Cpu::new(pc) // Cold IRQ entry must not fill current/decode slots.
};
memory.write32(RAM, 0xffff_ffff).unwrap();
memory.write32(RAM + 4, 0x5678_1234).unwrap();
memory.write32(RAM + 8, 0x9abc_def0).unwrap();
seeded_irq(&mut memory);
let mut machine = Machine::new(cpu, memory);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
let expected = if thumb {
if offset == 0 {
0xa1b2_1234
} else {
0x5678_c3d4
}
} else {
0x9abc_def0
};
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(expected));
// Missing BIOS vector fails next step, without reverting accepted IRQ history.
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(expected));
}
}
}
#[test]
fn irq_fetches_do_not_commit_a_bios_instruction_snapshot_or_erase_local_history() {
for pc in [0x100, ROM_START, 0x0e00_0000] {
let mut memory = Memory::with_bios(vec![], vec![0; BIOS_SIZE]).unwrap();
memory.bios_prefetch = Some(SEED);
seeded_irq(&mut memory);
let mut machine = Machine::new(Cpu::new(pc), memory);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.memory().bios_prefetch, Some(SEED));
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(SEED));
// BIOS retained history still follows the successful-instruction policy.
machine.step().unwrap();
assert_eq!(machine.memory().bios_prefetch, Some(0));
}
}

View file

@ -89,6 +89,7 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
}
}
/// Target/decode pair only. The caller separately charges the old-state source fetch.
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
+ bus_cycles(
@ -97,12 +98,6 @@ pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u3
width,
AccessKind::Sequential,
)
+ bus_cycles(
waitcnt,
target.wrapping_add(2 * width.bytes()),
width,
AccessKind::Sequential,
)
}
/// A data-only trace. Instruction fetches and branch refills are charged by the

View file

@ -65,7 +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);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff4);
// IRQ entry gains five cycles before the timer stops; return loses five afterward.
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 0xfff9);
for _ in 0..100 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
@ -84,7 +85,7 @@ fn overflow_after_instruction_is_sampled_on_next_step() {
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
assert_eq!(machine.cycles(), 9); // New ROM fetch at PC+8: 6; nominal BIOS IRQ refill: 3
assert_eq!(machine.cycles(), 14); // Instruction fetch 6; IRQ source fetch 6 + BIOS pair 2.
}
#[test]
@ -92,7 +93,7 @@ fn missing_vector_errors_only_after_completed_irq_entry() {
let mut bus = Memory::new(vec![]).unwrap();
pending_timer(&mut bus);
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // No ROM fetch required
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); // Missing discarded ROM fetch cannot prevent IRQ entry
let before = machine.cpu().clone();
let cycles = machine.cycles();
assert_eq!(

View file

@ -128,8 +128,11 @@ fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
#[test]
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
// 20-cycle timer: four 6-cycle fetches or five 4-cycle fetches, then 3-cycle IRQ entry.
for (waitcnt, entry_step, instructions, cycles) in [(0, 5, 4, 27), (0x18, 6, 5, 23)] {
// 20-cycle timer: four 6-cycle fetches or five 4-cycle fetches.
// IRQ entry adds one old-state ROM fetch and the two-cycle BIOS vector pair.
for (waitcnt, entry_step, instructions, cycles, irq_cycles) in
[(0, 5, 4, 32, 8), (0x18, 6, 5, 26, 6)]
{
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
memory.write16(WAITCNT, waitcnt).unwrap();
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
@ -150,7 +153,7 @@ fn wait_states_change_when_a_timer_interrupt_is_sampled() {
}
assert_eq!(machine.cpu().registers()[0], instructions);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing().code_cycles, 3);
assert_eq!(machine.last_timing().code_cycles, irq_cycles);
}
}

View file

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

View file

@ -92,7 +92,9 @@ The first target instruction samples T+2W before execution. State transitions re
Thumb BL's prefix advances sequentially; only its suffix refills. Ordinary Thumb PC writes preserve Thumb state.
The buffer records both expected PC and instruction state. A state mismatch cannot reinterpret retained ARM words as Thumb halfwords.
Machine IRQ entry captures the ARM vector pair before any handler instruction runs.
Machine IRQ entry first samples and discards the old-state P+2W fetch, then captures the ARM vector pair.
The discarded fetch drives actual IWRAM lanes but does not execute an instruction or commit a BIOS instruction snapshot.
Missing source bytes cannot prevent IRQ entry. Missing vector bytes fail only when the handler instruction becomes current.
`Cpu::enter_exception` and `Cpu::take_interrupt` have no memory parameter: they invalidate the buffer and leave vector sampling to the next step.
An unexpected PC discontinuity starts from current mapped bytes rather than reusing a mismatched buffer.
@ -108,9 +110,10 @@ Use it after debugger code repair or when attaching different memory to a retain
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.
Samples add no nominal data accesses or device cycles. Existing refill costs and one-shot DMA resume costs remain unchanged.
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, exact refill costs, per-access device updates, and DMA arbitration remain incomplete.
Complete fetch-driven bus history, access-kind sequencing, 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).
@ -234,11 +237,12 @@ Failed instructions do not sample a target or replace committed history.
Target samples use strict mapped reads and add no nominal data or device cycles.
The same captured target pair fills the CPU buffer and updates IWRAM history; no second target read occurs.
A failed target fetch remains a later instruction diagnostic.
IRQ entry samples its ARM BIOS vector pair without erasing IWRAM lanes.
IRQ entry updates IWRAM lanes if its discarded old-state fetch addresses IWRAM.
Its subsequent ARM BIOS vector pair does not erase those lanes.
A BIOS handler can still change those lanes through actual IWRAM data accesses.
The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a fully timed pipeline.
Sub-instruction DMA ordering and exact refill timing remain incomplete.
Sub-instruction DMA ordering and complete bus sequencing remain incomplete.
### DMA effects on Thumb IWRAM continuations
@ -317,17 +321,19 @@ An ordinary arithmetic instruction costs 1S. A register-specified shift adds 1I,
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.
Branches, PC writes, software interrupts, and exception entry use a nominal 1N+2S code refill.
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.
Thumb `BL` charges 1S for its prefix and 1N+2S for its suffix.
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.
Non-refill 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.
A 128 KiB ROM boundary therefore forces N timing when lookahead reaches it, not when execution reaches it later.
This applies to cold and retained pipelines without adding separate startup-fill costs.
See [fetch-address timing](../research/fetch-address-timing.md) for evidence, examples, and remaining limits.
See [fetch-address timing](../research/fetch-address-timing.md) and [refill timing](../research/refill-fetch-timing.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.
@ -346,7 +352,8 @@ The new settings apply to subsequent code accesses.
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential.
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
A non-refill instruction uses its 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.
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.
@ -356,16 +363,15 @@ Host access, clock-only advancement, and HALT/STOP idle do not consume it.
A failed DMA unit cannot create pending resume.
DMA WAITCNT writes apply before resume; CPU WAITCNT writes still use the previous code-access settings.
Branch and exception refill summaries retain their existing destination-based `1N+2S` costs, without an added resume access.
The old-PC fetch is not modeled separately. This remains nominal timing, not a per-access pipeline.
A resumed refill can have two N accesses: the old-PC source fetch and the first target fetch.
The resume changes the source access kind; it does not append another access.
This remains nominal timing, not a per-access pipeline.
See [the evidence and limitations](../research/dma-resume-timing.md).
Important timing limits:
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline.
- Non-refill code costs follow actual lookahead addresses, but cold current/decode fills have no separate startup charge.
- PC writes still use destination-region costs and the restored instruction width for nominal refill accesses.
Their old-PC fetch and target pair are not yet charged individually; IRQ entry has the same remaining limit.
- Source S/N kinds follow instruction-local summaries, not complete neighboring instruction/data-access sequencing.
- Code costs follow actual source and target-pair addresses, but cold current/decode fills have no separate startup charge.
- 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

@ -11,6 +11,9 @@ Each successful step advances display and timer clocks by its nominal cost.
`Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step.
`StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work.
IRQ entry is a separate step; the vector instruction runs on the following step.
Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pair at `0x18` and `0x1c`.
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
Bus writes take effect before the step's device-clock update.
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.

View file

@ -180,6 +180,15 @@ Fetch-address timing research:
See [fetch-address timing](research/fetch-address-timing.md). This corrects non-refill address selection, not full bus sequencing or refill timing.
Refill and IRQ fetch-timing research:
- The ARM7TDMI branch sequence above, for the discarded source fetch before the destination pair.
- [ARM7TDMI software-interrupt and exception entry](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/software-interrupt-and-exception-entry), for the incoming-state `pc+2L` access followed by the ARM vector pair.
- The pinned NanoBoyAdvance `SignalIRQ`, `Run`, `ReloadPipeline16`, and `ReloadPipeline32` methods above, for discarded incoming-state fetches and N/S target pairs.
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.
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

@ -69,7 +69,8 @@ Failed-instruction tests continue to assert full-state preservation.
This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline.
Cold fill and target-pair samples do not add nominal cycles or data accesses.
The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged.
The 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.

View file

@ -27,7 +27,7 @@ A failed unit cannot create or consume it. A blocked-source unit that successful
Register configuration, host reads/writes, and clock-only advancement do not consume it.
Before instruction execution, `Cpu::step_timed` captures the pending state and WAITCNT.
For a non-refill instruction, pending resume changes the nominal code-access kind to non-sequential.
Pending resume changes the source code-access kind to non-sequential.
The existing bus-cost function applies the code width and ROM-window wait settings.
An ARM word still uses a non-sequential first halfword and a sequential second halfword.
This is a change of access kind, not a fixed positive surcharge: some wait settings make N cheaper than S.
@ -50,9 +50,9 @@ The [fetch-address timing extension](fetch-address-timing.md) now charges non-re
A resumed access selects that fetch region's wait settings. It is still not a separately scheduled bus event.
DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
Existing refill summaries remain destination-based `1N+2S` costs.
A resumed branch or exception consumes pending state without adding another nominal access.
This prevents double-counting the existing N component, but does not model the discarded old-PC fetch separately.
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.
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.

View file

@ -63,18 +63,18 @@ Timer tests and the original timer demo now reflect this bounded rule.
## Deliberately retained limits
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.
- Branches, PC loads/writes, SWI, exception returns, and IRQ entry retain destination-based `1N+2S` summaries.
The old-PC fetch and target pair are not yet charged individually.
- The new lookahead cost is not added to those refill summaries; doing so would count an extra access.
- Thumb BL prefixes use their lookahead address; suffixes retain the nominal refill summary.
- 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.
- Devices advance once after the whole successful instruction. No per-access timer/IRQ update is introduced.
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
The next timing step is verified source-fetch/target-pair accounting for refills and IRQ entry.
The next timing step is verified access-kind sequencing between instructions and data accesses.
## Original regression coverage

View file

@ -77,7 +77,8 @@ A retry takes a new lookahead sample from current memory.
- All data reads within an instruction still use one entry snapshot.
The later [fetch-address timing extension](fetch-address-timing.md) charges non-refill code at the actual lookahead address.
Refill summaries remain destination-based; the instruction-local access kinds and cold-start costs are still nominal.
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.
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
No boundary timing accuracy is claimed.

View file

@ -51,7 +51,8 @@ Debugger invalidation clears only instruction retention, but the following cold
A successful instruction commits its staged lanes regardless of its resulting state, PC, or region.
A failed instruction discards every staged change, including cold-fill samples.
Successful PC-writing instructions then apply target fetches after their data accesses.
Other-region execution, BIOS IRQ entry, and unavailable targets do not erase known local lanes.
Other-region execution, BIOS vector fetches, and unavailable targets do not erase known local lanes.
The later [IRQ fetch extension](refill-fetch-timing.md) updates addressed IWRAM lanes through its discarded old-state source fetch.
Host inspection and setup never drive the latch.
Initial lanes remain unknown until observed accesses establish them.

View file

@ -0,0 +1,122 @@
# Source-fetch and target-pair refill timing
## Finding and evidence
The instruction buffer already sampled a branch's old-state lookahead and its two target instructions.
However, timing charged three destination accesses: N(T), S(T+W), and S(T+2W).
That omitted the source-region cost and charged the third target fetch before target execution.
The totals often matched within one memory region, hiding cross-region and instruction-state errors.
Machine IRQ entry also omitted the discarded source sample entirely.
The [ARM7TDMI branch timing description](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/branch-and-branch-with-link) specifies three accesses:
1. Prefetch from the current PC while calculating the destination. The decision cannot prevent this fetch.
2. Fetch the branch destination.
3. Fetch destination plus one instruction length to refill the pipeline.
The [software-interrupt and exception-entry description](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/software-interrupt-and-exception-entry) likewise describes a forced-address construction cycle followed by pipeline refill.
Its first address is `pc+2L`, using the incoming instruction state.
The vector pair uses ARM state.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) provides a source cross-check:
- `Run` fetches the old-state lookahead before instruction execution.
- `ReloadPipeline16` and `ReloadPipeline32` read the target N and the next instruction S.
- `SignalIRQ` first performs a discarded halfword/word fetch using the incoming state and access kind.
It then changes mode/state and reloads the ARM vector pair.
This supports source-address and target-pair accounting, not complete chronological bus sequencing in this emulator.
The existing [DMA resume evidence](dma-resume-timing.md) supports changing the next source access to N.
No physical-hardware measurement or external-emulator differential run was performed.
Tests and tracing code are original. No external source code, ROM, or BIOS was added to the repository.
## Implemented subset
For an instruction executing at P, `Cpu::step_timed` charges its sampled lookahead once:
- 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.
- WAITCNT: captured before instruction effects.
If the instruction refills, timing also charges N(T) and S(T+W) using the resulting state.
The existing pipeline samples supply those instructions and update supported local history.
T+2W is sampled and charged when the first target instruction executes, not during the refill.
The same rule covers taken branches, BX, PC loads/writes, SWI, saved-state returns, and Thumb BL suffixes.
Untaken conditions and BL prefixes do not add a target pair.
Accepted machine IRQ entry samples the discarded incoming-state P+2W fetch, then the ARM vector pair.
It does not fetch or execute the interrupted current opcode, nor invent a cold current/decode fill.
The discarded sample drives its addressed IWRAM lanes when mapped; other regions leave that local history unchanged.
An unavailable discarded sample cannot prevent entry and is not retained as a current-slot diagnostic.
Unavailable vector samples fail only when execution reaches them.
IRQ sampling does not commit a BIOS instruction snapshot because no BIOS instruction executed.
The separate successful-BIOS-instruction history policy remains unchanged.
Masked IRQ requests have no fetch, history, CPU, or resume-state effects.
DMA still takes priority at machine-step boundaries.
Successful IRQ entry consumes pending DMA resume through normal machine-step completion.
Devices advance once by the total entry cost, not between its accesses.
The public memory-free `Cpu::enter_exception` and `Cpu::take_interrupt` APIs remain unchanged.
## Examples with default WAITCNT
| Operation | Source | Target pair | Code cycles |
| --- | --- | --- | --- |
| IWRAM ARM BX to Thumb WS1 | 1 | 5 + 5 | 11 |
| IWRAM Thumb BX to ARM WS0 | 1 | 8 + 6 | 15 |
| ARM WS0 branch to IWRAM | 6 | 1 + 1 | 8 |
| ARM WS0 branch within WS0 | 6 | 8 + 6 | 20 |
| Same branch after DMA | 8 | 8 + 6 | 22 |
| IRQ from ARM WS0 | 6 | 1 + 1 | 8 |
| IRQ from ARM WS0 after DMA | 8 | 1 + 1 | 10 |
These examples exclude ROM page boundaries.
Every access independently selects its actual region, wait window, and forced-N 128 KiB boundary cost.
A target pair can cross a region or wait-window boundary.
A boundary at T+2W is charged on target execution, not during the preceding branch.
DMA can produce two N accesses: the source fetch and the first target fetch. No fourth fetch is added.
## Original regression coverage
Tests cover:
- Incoming/outgoing ARM and Thumb widths, all ROM wait settings, and the stored prefetch bit.
- Boundaries in the first, second, and third target slots, plus split wait-window target pairs.
- ROM source boundaries and DMA resume before a RAM refill, with no delayed resume penalty.
- Cross-region source lookahead, data/internal costs for PC loads, and saved-state returns.
- IRQ source widths, missing ROM, masked requests, LR/SPSR preservation, and device-clock totals.
- IRQ IWRAM word and halfword lane updates, cold entry without invented fills, and retained Thumb entry.
- Missing-vector rollback after accepted entry, BIOS-history isolation, and timed/untimed CPU equivalence.
## Validation and report changes
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 fetch-address change, the ARM, Thumb, and memory reports each lose five nominal cycles.
Their step counts and all other report fields remain unchanged.
The BIOS report gains three instructions and 29 nominal cycles; all other fields remain unchanged.
A temporary original trace runner compared the preceding and updated cores against the pinned BIOS fixture.
The changed VBlank arrival position causes one extra three-instruction polling loop, adding 34 cycles.
Cross-region firmware transfers and IRQ entry together remove five cycles, giving the observed net increase of 29.
The trace still reaches the same checkpoint with r12=0 and one IRQ entry.
This trace comparison is a regression audit between our core versions, not external hardware validation.
The original timer demo retains its total nominal duration and IRQ step.
IRQ entry gains five cycles before the handler stops the timer; the return loses five cycles afterward.
The stopped counter therefore increases by five even though the final machine-cycle total is unchanged.
## 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.
- 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.

View file

@ -39,6 +39,7 @@ It does not replay the bus observations of retained current/decode instructions.
The consumer performs no mapped memory read.
After successful control flow, the same target samples fill the instruction buffer and update local IWRAM history in either state.
Machine IRQ entry uses this refill path; BIOS vector fetches leave IWRAM lanes unchanged.
The later [refill timing extension](refill-fetch-timing.md) first samples the discarded source fetch, which can update IWRAM lanes.
Instruction buffers and BIOS/IWRAM latches remain separate retained state.
Sampling alone changes no CPU access context, latch, device clock, DMA resume marker, or data-timing trace.
@ -56,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).
- Destination-based refill summaries and instruction-local access kinds remain nominal.
- Refills now charge the source fetch and target pair; instruction-local source access kinds remain nominal.
- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete.
## Original tests

View file

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

View file

@ -45,7 +45,8 @@ Update this file when a feature lands or a limit is removed.
- STOP freezes CPU/DMA/device clocks, retains device phases, and reports a stopped frame runner without spinning.
- Machine execution to the next VBlank event, with a configurable step limit.
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
- Non-refill code costs use actual ARM PC+8/Thumb PC+4 fetch addresses, including memory-region and ROM wait-window boundaries.
- 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.
- 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.
@ -79,7 +80,7 @@ Update this file when a feature lands or a limit is removed.
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation.
- Thumb unused-memory reads follow the actual PC+4 fetch region, including mapped boundaries, ROM windows, and IWRAM lane history.
- Persistent IWRAM lanes track ARM/Thumb fetches, CPU data accesses, and successful DMA, with known-bit tracking and diagnostic rollback.
- Cold fills and ARM/Thumb target pairs update local history; state changes and BIOS IRQ entry do not erase it.
- Cold fills, ARM/Thumb target pairs, and discarded IRQ source fetches update addressed IWRAM lanes; state changes do not erase them.
- Little-endian byte, halfword, and word reads/writes.
- ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior.
- Explicit errors for unsupported instructions when their condition passes, and for unsupported memory accesses.
@ -141,7 +142,8 @@ ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot]
Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and persistent local IWRAM lanes.
Only actual IWRAM fetches and data accesses drive those lanes, independently of CPU state and executing region.
Cold fills and target pairs establish both lanes when all required samples are mapped.
State changes, other-region execution, and BIOS IRQ entry preserve local history; failed instructions discard staged updates.
State changes and other-region accesses preserve local history; an IRQ source fetch updates lanes when it addresses IWRAM.
Failed instructions discard staged updates.
Successful DMA can establish lanes before CPU startup. Resumed fetches then update their addressed lanes.
ARM/Thumb instruction buffering retains sequential instructions and refill target pairs.
Instruction buffering and supported bus history now consume the same mapped fetch samples, without sharing retained storage.
@ -162,8 +164,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. Replace destination-only refill summaries with verified source-fetch/target-pair timing, including IRQ entry.
Then extend access-kind sequencing, startup costs, Game Pak prefetch, per-access device updates, and timer/IRQ delays.
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.
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

@ -83,7 +83,7 @@ They check both target alignments, mirrors/wrap, captured target+2 versus later
Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA lane effects, and BIOS isolation retain checks.
DMA tests compare source/destination lane updates against a byte-array reference, then apply the resumed PC+4 fetch.
They cover channel preemption, blocked sources, failures, mirrors, pre-start lane establishment, cold fills, and other-region snapshot isolation.
IRQ tests verify local-latch preservation; PC discontinuities verify new pipeline-fill samples.
IRQ tests verify discarded-source lane updates and preservation across non-IWRAM fetches; PC discontinuities verify new pipeline-fill samples.
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.
@ -252,9 +252,13 @@ The bounded graphics frame runner is checked for instruction errors and timeouts
Timing tests cover every ROM wait-state setting, access width, ROM window, and 128 KiB boundary handling.
Fetch-address regressions place executing instructions before memory-region and ROM page/wait-window boundaries.
They check ARM/Thumb widths, cold/retained pipelines, independent N/S tables, DMA resume, and pre-write WAITCNT settings.
Other checks cover deferred fetch errors, unchanged nominal refill totals, timer advancement, and subsequent IRQ sampling.
Other checks cover deferred fetch errors, source-plus-target refill totals, timer advancement, and subsequent IRQ sampling.
Startup-ROM and timer expectations now use P+8/P+4 rather than the executing address; data and rollback assertions remain intact.
See [fetch-address timing](research/fetch-address-timing.md) for the bounded scope.
Refill timing tests cover incoming/outgoing widths, all ROM wait settings, and target first/second/third-slot page boundaries.
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 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.