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