feat(cpu): account for source fetches in branch and IRQ refills
This commit is contained in:
parent
0650b60908
commit
2d7eb6e4eb
27 changed files with 638 additions and 87 deletions
|
|
@ -56,6 +56,8 @@ mod load_alias_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod open_bus_tests;
|
mod open_bus_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod refill_timing_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod status_tests;
|
mod status_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
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.
|
let (mut cpu, mut bus) = program(false, PC, 0xea00_0000); // B PC+8.
|
||||||
dma(&mut bus);
|
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);
|
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 6);
|
||||||
|
|
||||||
let (cpu, _) = program(false, PC, 0xe1a0_0000);
|
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);
|
let mut machine = Machine::new(cpu, bus);
|
||||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
|
||||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
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.
|
// Inspect the bus through a separate CPU after IRQ entry, before a handler runs.
|
||||||
let mut probe = Cpu::new(PC);
|
let mut probe = Cpu::new(PC);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
probe.step_timed(machine.memory_mut()).unwrap().code_cycles,
|
probe.step_timed(machine.memory_mut()).unwrap().code_cycles,
|
||||||
6
|
6
|
||||||
);
|
);
|
||||||
assert_eq!(machine.memory().cycles(), 12);
|
assert_eq!(machine.memory().cycles(), 19); // DMA 9 + IRQ entry 10.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -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 super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
dma::{DMA_BASE, DMA_STRIDE},
|
dma::{DMA_BASE, DMA_STRIDE},
|
||||||
|
|
@ -189,7 +189,7 @@ fn unavailable_lookahead_keeps_nominal_address_cost_and_fails_only_when_executed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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] {
|
for thumb in [false, true] {
|
||||||
let width = if thumb { 2 } else { 4 };
|
let width = if thumb { 2 } else { 4 };
|
||||||
let (mut cpu, mut memory) = program(thumb, ROM_START - width, 32);
|
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();
|
memory.write32(cpu.pc(), 0xe12f_ff11).unwrap();
|
||||||
}
|
}
|
||||||
cpu.registers[1] = DEST | u32::from(thumb);
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@ fn successful_dma_between_refill_and_target_execution_updates_refill_history() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 thumb in [false, true] {
|
||||||
for destination in [TARGET, TARGET + 2] {
|
for destination in [TARGET, TARGET + 2] {
|
||||||
let (cpu, mut bus) = branch(ROM_START, thumb, destination);
|
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.cpu(), &untimed);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
machine.last_timing().code_cycles,
|
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().data_cycles, index);
|
||||||
assert_eq!(machine.last_timing().internal_cycles, index);
|
assert_eq!(machine.last_timing().internal_cycles, index);
|
||||||
}
|
}
|
||||||
assert_eq!(machine.cycles(), 6);
|
let cycles = if thumb { 8 } else { 11 }; // Source fetch, target pair, then 1S+1N+1I load.
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6);
|
assert_eq!(machine.cycles(), cycles);
|
||||||
|
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), cycles as u16);
|
||||||
assert_eq!(bus.cycles(), 0);
|
assert_eq!(bus.cycles(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
279
crates/gba-core/src/cpu/refill_timing_tests.rs
Normal file
279
crates/gba-core/src/cpu/refill_timing_tests.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
|
||||||
//! Non-refill code costs use the new fetch address; data costs use actual bus accesses.
|
//! Code costs use source-fetch and target-pair addresses; data costs use actual bus accesses.
|
||||||
//! Access kinds and PC-write refill totals remain instruction-local nominal summaries.
|
//! Source access kinds remain instruction-local nominal summaries.
|
||||||
|
|
||||||
use super::{Cpu, CpuError, InstructionSet};
|
use super::{Cpu, CpuError, InstructionSet};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -65,11 +65,10 @@ impl Cpu {
|
||||||
let result = self.execute_fetched(fetched, memory);
|
let result = self.execute_fetched(fetched, memory);
|
||||||
let data_cycles = memory.end_data_timing();
|
let data_cycles = memory.end_data_timing();
|
||||||
result?;
|
result?;
|
||||||
let code_cycles = if summary.refill {
|
let mut code_cycles = bus_cycles(waitcnt, fetch_address, width, code_kind);
|
||||||
refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width())
|
if summary.refill {
|
||||||
} else {
|
code_cycles += refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width());
|
||||||
bus_cycles(waitcnt, fetch_address, width, code_kind)
|
}
|
||||||
};
|
|
||||||
Ok(StepTiming {
|
Ok(StepTiming {
|
||||||
code_cycles,
|
code_cycles,
|
||||||
data_cycles,
|
data_cycles,
|
||||||
|
|
@ -78,16 +77,24 @@ impl Cpu {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nominal refill cost after explicit exception entry. No vector bytes are read.
|
/// Accept IRQ between instructions, discard the old-state fetch, then refill ARM.
|
||||||
pub(crate) fn exception_timing(&self, memory: &Memory) -> StepTiming {
|
/// A missing discarded fetch cannot prevent entry or become a current-slot error.
|
||||||
StepTiming {
|
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||||
code_cycles: refill_cycles(
|
let state = self.instruction_set;
|
||||||
memory.waitcnt(),
|
let address = self.pc().wrapping_add(2 * state.width());
|
||||||
self.pc(),
|
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||||
self.instruction_set.access_width(),
|
return None;
|
||||||
),
|
|
||||||
..StepTiming::default()
|
|
||||||
}
|
}
|
||||||
|
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 {
|
fn arm_summary(&self, instruction: u32) -> Summary {
|
||||||
|
|
@ -198,7 +205,7 @@ impl Cpu {
|
||||||
Summary::branch()
|
Summary::branch()
|
||||||
}
|
}
|
||||||
0xdf00..=0xdfff | 0xe000..=0xe7ff | 0xf800..=0xffff => 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(),
|
_ => Summary::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,14 +78,14 @@ fn branch_and_pc_write_refill_even_when_target_equals_fallthrough() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
let (mut cpu, mut memory) = arm(0xe12f_ff10);
|
||||||
cpu.registers[0] = 0x0a00_0101; // Thumb in WS1: 5N + 5S + 5S
|
cpu.registers[0] = 0x0a00_0101; // IWRAM source 1 + Thumb WS1 target pair 5N + 5S.
|
||||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
|
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(11, 0, 0));
|
||||||
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
|
||||||
let (mut cpu, mut memory) = thumb(0x4700);
|
let (mut cpu, mut memory) = thumb(0x4700);
|
||||||
cpu.registers[0] = ROM_START + 0x100; // ARM in WS0: 8N + 6S + 6S
|
cpu.registers[0] = ROM_START + 0x100; // IWRAM source 1 + ARM WS0 target pair 8N + 6S.
|
||||||
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(20, 0, 0));
|
assert_eq!(cpu.step_timed(&mut memory).unwrap(), expected(15, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
let (mut cpu, mut memory) = arm(0xe590_f000);
|
||||||
cpu.registers[0] = DATA;
|
cpu.registers[0] = DATA;
|
||||||
memory.write32(DATA, ROM_START + 0x100).unwrap();
|
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]
|
#[test]
|
||||||
|
|
@ -321,7 +321,7 @@ fn software_interrupts_and_exception_returns_use_new_code_region_and_state() {
|
||||||
cpu.registers[14] = ROM_START + 0x100;
|
cpu.registers[14] = ROM_START + 0x100;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cpu.step_timed(&mut memory).unwrap(),
|
cpu.step_timed(&mut memory).unwrap(),
|
||||||
expected(if thumb_state { 11 } else { 20 }, 0, 0)
|
expected(if thumb_state { 9 } else { 15 }, 0, 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,8 @@ impl Machine {
|
||||||
self.last_timing = timing;
|
self.last_timing = timing;
|
||||||
return Ok(StepKind::HaltIdle);
|
return Ok(StepKind::HaltIdle);
|
||||||
}
|
}
|
||||||
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
|
let (kind, timing) = if let Some(timing) = self.cpu.take_irq_timed(&mut self.memory) {
|
||||||
self.cpu.refill_pipeline(&mut self.memory);
|
(StepKind::IrqEntry, timing)
|
||||||
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
|
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
StepKind::Instruction,
|
StepKind::Instruction,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ use std::{cell::Cell, error::Error, fmt};
|
||||||
mod fetch;
|
mod fetch;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod fetch_tests;
|
mod fetch_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod irq_fetch_tests;
|
||||||
mod iwram_bus;
|
mod iwram_bus;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod iwram_history_tests;
|
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.
|
/// Successful IWRAM target fetches drive their lanes in order, in either CPU state.
|
||||||
/// Other-region and unavailable target fetches leave the local latch unchanged.
|
/// Other-region and unavailable target fetches leave the local latch unchanged.
|
||||||
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
|
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
|
||||||
|
|
|
||||||
90
crates/gba-core/src/memory/irq_fetch_tests.rs
Normal file
90
crates/gba-core/src/memory/irq_fetch_tests.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
|
||||||
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
|
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
|
||||||
+ bus_cycles(
|
+ bus_cycles(
|
||||||
|
|
@ -97,12 +98,6 @@ pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u3
|
||||||
width,
|
width,
|
||||||
AccessKind::Sequential,
|
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
|
/// A data-only trace. Instruction fetches and branch refills are charged by the
|
||||||
|
|
|
||||||
|
|
@ -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.cpu().registers()[10], 1);
|
||||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
assert_eq!(machine.memory().read16(TIMER_BASE + 2).unwrap(), 0);
|
||||||
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 {
|
for _ in 0..100 {
|
||||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
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.step().unwrap(), StepKind::IrqEntry);
|
||||||
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
|
assert_eq!(machine.cpu().registers()[0], 1); // Interrupted instruction did not execute
|
||||||
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
|
assert_eq!(machine.cpu().registers()[14], ROM_START + 8);
|
||||||
assert_eq!(machine.cycles(), 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]
|
#[test]
|
||||||
|
|
@ -92,7 +93,7 @@ fn missing_vector_errors_only_after_completed_irq_entry() {
|
||||||
let mut bus = Memory::new(vec![]).unwrap();
|
let mut bus = Memory::new(vec![]).unwrap();
|
||||||
pending_timer(&mut bus);
|
pending_timer(&mut bus);
|
||||||
let mut machine = Machine::new(Cpu::new(ROM_START), 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 before = machine.cpu().clone();
|
||||||
let cycles = machine.cycles();
|
let cycles = machine.cycles();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,11 @@ fn stored_prefetch_phi_and_sram_settings_do_not_change_supported_bus_costs() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
|
fn wait_states_change_when_a_timer_interrupt_is_sampled() {
|
||||||
// 20-cycle timer: four 6-cycle fetches or five 4-cycle fetches, then 3-cycle IRQ entry.
|
// 20-cycle timer: four 6-cycle fetches or five 4-cycle fetches.
|
||||||
for (waitcnt, entry_step, instructions, cycles) in [(0, 5, 4, 27), (0x18, 6, 5, 23)] {
|
// 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();
|
let mut memory = Memory::new(0xe280_0001_u32.to_le_bytes().repeat(64)).unwrap();
|
||||||
memory.write16(WAITCNT, waitcnt).unwrap();
|
memory.write16(WAITCNT, waitcnt).unwrap();
|
||||||
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
|
memory.write32(TIMER_BASE, 0x00c0_ffec).unwrap(); // 20 cycles
|
||||||
|
|
@ -150,7 +153,7 @@ fn wait_states_change_when_a_timer_interrupt_is_sampled() {
|
||||||
}
|
}
|
||||||
assert_eq!(machine.cpu().registers()[0], instructions);
|
assert_eq!(machine.cpu().registers()[0], instructions);
|
||||||
assert_eq!(machine.cycles(), cycles);
|
assert_eq!(machine.cycles(), cycles);
|
||||||
assert_eq!(machine.last_timing().code_cycles, 3);
|
assert_eq!(machine.last_timing().code_cycles, irq_cycles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, deferred fetch diagnostics, refill sampling, and explicit invalidation |
|
||||||
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
|
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
|
||||||
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence |
|
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence |
|
||||||
| `crates/gba-core/src/cpu/timing.rs` | 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/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/dma_resume_tests.rs` | One-shot non-sequential DMA resume costs, wait settings, failures, idle, IRQs, and API isolation |
|
||||||
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |
|
||||||
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
| `crates/gba-demos/src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
`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.
|
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.
|
Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code behavior.
|
||||||
|
|
||||||
This is instruction buffering, not a complete timed fetch pipeline.
|
This is instruction buffering, not a complete timed fetch pipeline.
|
||||||
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.
|
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),
|
See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md), [Thumb extension](../research/thumb-instruction-buffer.md),
|
||||||
and [shared fetch samples](../research/shared-fetch-samples.md).
|
and [shared fetch samples](../research/shared-fetch-samples.md).
|
||||||
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
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.
|
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
|
### 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.
|
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.
|
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.
|
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.
|
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.
|
Multiply costs vary with the incoming multiplier's upper bytes; accumulate and long forms add internal cycles.
|
||||||
ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register.
|
ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register.
|
||||||
|
|
||||||
Non-refill code costs use the new instruction fetch address: P+8 for ARM and P+4 for Thumb.
|
Source code costs use the new instruction fetch address: P+8 for ARM and P+4 for Thumb.
|
||||||
The incoming instruction state selects the width. The sampled fetch address selects the memory region and ROM wait-state window.
|
The incoming instruction state selects the width. The sampled fetch address selects the memory region and ROM wait-state window.
|
||||||
A 128 KiB ROM boundary therefore forces N timing when lookahead reaches it, not when execution reaches it later.
|
A 128 KiB ROM boundary therefore forces N timing when lookahead reaches it, not when execution reaches it later.
|
||||||
This applies to cold and retained pipelines without adding separate startup-fill costs.
|
This applies to cold and retained pipelines without adding separate startup-fill costs.
|
||||||
See [fetch-address timing](../research/fetch-address-timing.md) 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.
|
Data costs use the addresses and widths of actual CPU bus calls, after alignment handling.
|
||||||
BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle.
|
BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle.
|
||||||
|
|
@ -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.
|
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential.
|
||||||
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
|
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
|
||||||
A non-refill instruction uses its 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.
|
ARM word accesses retain an S cost for their second halfword.
|
||||||
Stores and ROM boundaries that already use N do not receive an additional cost.
|
Stores and ROM boundaries that already use N do not receive an additional cost.
|
||||||
|
|
||||||
|
|
@ -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.
|
A failed DMA unit cannot create pending resume.
|
||||||
DMA WAITCNT writes apply before resume; CPU WAITCNT writes still use the previous code-access settings.
|
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.
|
A resumed refill can have two N accesses: the old-PC source fetch and the first target fetch.
|
||||||
The old-PC fetch is not modeled separately. This remains nominal timing, not a per-access pipeline.
|
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).
|
See [the evidence and limitations](../research/dma-resume-timing.md).
|
||||||
|
|
||||||
Important timing limits:
|
Important timing limits:
|
||||||
|
|
||||||
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline.
|
- Source S/N kinds follow instruction-local summaries, not complete neighboring instruction/data-access sequencing.
|
||||||
- Non-refill code costs follow actual lookahead addresses, but cold current/decode fills have no separate startup charge.
|
- Code costs follow actual source and target-pair addresses, but cold current/decode fills have no separate startup charge.
|
||||||
- PC writes still use destination-region costs and the restored instruction width for nominal refill accesses.
|
|
||||||
Their old-PC fetch and target pair are not yet charged individually; IRQ entry has the same remaining limit.
|
|
||||||
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
|
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
|
||||||
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
|
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
|
||||||
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.
|
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
`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.
|
`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.
|
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.
|
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.
|
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.
|
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
||||||
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
||||||
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
The ignore file excludes `roms/`, common game ROM extensions, and save files.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline.
|
||||||
Cold fill and target-pair samples do not add nominal cycles or data accesses.
|
Cold fill and target-pair samples do not add nominal cycles or data accesses.
|
||||||
The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged.
|
The 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.
|
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.
|
Access kinds, cold-fill startup costs, and refill scheduling remain incomplete.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
A resumed access selects that fetch region's wait settings. It is still not a separately scheduled bus event.
|
||||||
DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
|
DMA still executes only between instructions, not between data accesses or during CPU internal cycles.
|
||||||
|
|
||||||
Existing refill summaries remain destination-based `1N+2S` costs.
|
The later [refill timing extension](refill-fetch-timing.md) replaces destination-only summaries with source-fetch and target-pair costs.
|
||||||
A resumed branch or exception consumes pending state without adding another nominal access.
|
Resume changes the source access to N; the first target access independently remains N.
|
||||||
This prevents double-counting the existing N component, but does not model the discarded old-PC fetch separately.
|
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.
|
Stores and fetches at 128 KiB ROM boundaries already use N and receive no duplicate cost.
|
||||||
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
|
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,18 +63,18 @@ Timer tests and the original timer demo now reflect this bounded rule.
|
||||||
|
|
||||||
## Deliberately retained limits
|
## 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.
|
- 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.
|
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.
|
- Refills now charge one source fetch plus the target N+S pair, not an additional fetch beside the old destination-only summary.
|
||||||
The old-PC fetch and target pair are not yet charged individually.
|
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
|
||||||
- The new lookahead cost is not added to those refill summaries; doing so would count an extra access.
|
|
||||||
- Thumb BL prefixes use their lookahead address; suffixes retain the nominal refill summary.
|
|
||||||
- Cold fills have no separate startup charge, even though their samples update supported bus history.
|
- 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.
|
- 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.
|
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
||||||
|
|
||||||
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
|
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
|
||||||
The next timing step is verified 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
|
## Original regression coverage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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.
|
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.
|
Per-access device updates, video-bus contention, general DMA handoff, and Game Pak prefetch remain incomplete.
|
||||||
No boundary timing accuracy is claimed.
|
No boundary timing accuracy is claimed.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 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.
|
A failed instruction discards every staged change, including cold-fill samples.
|
||||||
Successful PC-writing instructions then apply target fetches after their data accesses.
|
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.
|
Host inspection and setup never drive the latch.
|
||||||
|
|
||||||
Initial lanes remain unknown until observed accesses establish them.
|
Initial lanes remain unknown until observed accesses establish them.
|
||||||
|
|
|
||||||
122
docs/research/refill-fetch-timing.md
Normal file
122
docs/research/refill-fetch-timing.md
Normal 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.
|
||||||
|
|
@ -39,6 +39,7 @@ It does not replay the bus observations of retained current/decode instructions.
|
||||||
The consumer performs no mapped memory read.
|
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.
|
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.
|
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.
|
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.
|
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.
|
- All unused-memory data reads within an instruction still use its entry snapshot.
|
||||||
- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.
|
- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.
|
||||||
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
|
- Non-refill timing now consumes the sample's address in the [fetch-address extension](fetch-address-timing.md).
|
||||||
- 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.
|
- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete.
|
||||||
|
|
||||||
## Original tests
|
## Original tests
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Buffer samples add no nominal cycles or data accesses.
|
||||||
Timed and untimed execution use the same buffer logic.
|
Timed and untimed execution use the same buffer logic.
|
||||||
Destination-based refill summaries 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.
|
The later [fetch-address timing extension](fetch-address-timing.md) selects non-refill and DMA-resume code costs from the new fetch address.
|
||||||
Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md).
|
Mapped boundary snapshots now follow the [actual fetch region](fetch-region-boundaries.md).
|
||||||
Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
|
Complete bus history, exact boundary timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- STOP freezes CPU/DMA/device clocks, retains device phases, and reports a stopped frame runner without spinning.
|
||||||
- Machine execution to the next VBlank event, with a configurable step limit.
|
- Machine execution to the next VBlank event, with a configurable step limit.
|
||||||
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
- CPU/device stepping with nominal ARM7 instruction costs and memory wait states.
|
||||||
- Non-refill code costs use actual ARM PC+8/Thumb PC+4 fetch addresses, including memory-region and ROM wait-window boundaries.
|
- 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.
|
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||||
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
||||||
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
||||||
|
|
@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- Little-endian byte, halfword, and word reads/writes.
|
||||||
- ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior.
|
- 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.
|
- 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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
|
||||||
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
||||||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||||
2. Replace destination-only refill summaries with verified source-fetch/target-pair timing, including IRQ entry.
|
2. Extend instruction-local source access kinds into verified neighboring instruction/data-access sequencing.
|
||||||
Then extend access-kind sequencing, startup costs, Game Pak prefetch, per-access device updates, and timer/IRQ delays.
|
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.
|
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||||
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
|
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
|
||||||
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.
|
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
Timed/untimed tests verify CPU results and nominal clock costs.
|
||||||
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
|
||||||
ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes.
|
ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes.
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
||||||
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
||||||
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue