use std::collections::HashMap;
use crate::common::{Asid, LineAddr, PhysAddr, Ppn, Vpn};
use crate::config::{BackendKind, Config, LoadPrefetcherConfig, PageBoundary, PrefetcherKind};
use crate::isa::csr;
use crate::isa::privileged::PrivilegeMode;
use crate::tests::support::builder::instruction::InstructionBuilder;
use crate::tests::support::harness::TestContext;
use crate::uarch::mmu::tlb::PageSize;
const RAM_BASE: u64 = 0x8000_0000;
const RAM_SIZE: usize = 0x80_0000;
const FIRST_TABLE_PPN: u64 = 0x8_0100;
const CODE: u64 = 0x8020_0000;
const FIRST_FRAME: u64 = 0x8040_0000;
const SECOND_FRAME: u64 = 0x8060_5000;
const GIGAPAGE_FRAME: u64 = 0x8000_0000;
const PTE_V: u64 = 1;
const PTE_LEAF_RWX_AD: u64 = 0b1100_1111;
const PTE_LEAF_X_A: u64 = 0b0100_1001;
const PMP_NAPOT: u8 = 0b0001_1000;
const PMP_RWX: u8 = 0b0000_0111;
const STRIDE: i32 = 256;
const LOADS: i32 = 16;
const DATA_REG: usize = 10;
const NEXT_PAGE_REG: usize = 11;
const DONE_REG: usize = 2;
const DONE: u64 = 7;
#[derive(Clone, Copy, Debug)]
pub enum Mode {
Sv39,
Sv48,
Sv57,
}
impl Mode {
const ALL: [Self; 3] = [Self::Sv39, Self::Sv48, Self::Sv57];
const fn levels(self) -> u32 {
match self {
Self::Sv39 => 3,
Self::Sv48 => 4,
Self::Sv57 => 5,
}
}
const fn satp_mode(self) -> u64 {
match self {
Self::Sv39 => csr::SATP_MODE_SV39,
Self::Sv48 => csr::SATP_MODE_SV48,
Self::Sv57 => csr::SATP_MODE_SV57,
}
}
const fn data_va(self) -> u64 {
match self {
Self::Sv39 => 0x0000_0000_4000_0000,
Self::Sv48 => 0x0000_4000_4000_0000,
Self::Sv57 => 0x00C0_4000_4000_0000,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Data {
TouchedSecondPage,
UntouchedSecondPage,
ExecuteOnlySecondPage,
PmpDeniedSecondPage,
Megapage,
Gigapage,
}
impl Data {
const fn first_frame(self) -> u64 {
match self {
Self::Gigapage => GIGAPAGE_FRAME,
_ => FIRST_FRAME,
}
}
}
struct PageTables {
root: u64,
next_ppn: u64,
children: HashMap<(u64, u64), u64>,
levels: u32,
}
impl PageTables {
fn new(levels: u32) -> Self {
Self {
root: FIRST_TABLE_PPN,
next_ppn: FIRST_TABLE_PPN + 1,
children: HashMap::new(),
levels,
}
}
fn map(&mut self, ctx: &mut TestContext, va: u64, pa: u64, leaf_level: u32, flags: u64) {
let mut table = self.root;
for level in (leaf_level + 1..self.levels).rev() {
let index = index_at(va, level);
table = if let Some(&child) = self.children.get(&(table, index)) {
child
} else {
let child = self.next_ppn;
self.next_ppn += 1;
write_pte(ctx, table, index, (child << 10) | PTE_V);
let _ = self.children.insert((table, index), child);
child
};
}
write_pte(ctx, table, index_at(va, leaf_level), ((pa >> 12) << 10) | flags);
}
}
const fn napot(base: u64, bytes: u64) -> u64 {
(base >> 2) | ((bytes >> 3) - 1)
}
const fn index_at(va: u64, level: u32) -> u64 {
(va >> (12 + 9 * level)) & 0x1ff
}
fn write_pte(ctx: &mut TestContext, table_ppn: u64, index: u64, pte: u64) {
ctx.sim.probe_mem_store(PhysAddr::new((table_ppn << 12) | (index * 8)), pte, 8);
}
fn program(touch_second_page: bool) -> Vec<u32> {
let i = InstructionBuilder::new;
let (data, next) = (DATA_REG as u32, NEXT_PAGE_REG as u32);
let mut program = Vec::new();
if touch_second_page {
program.push(i().ld(13, next, 0).build());
}
program.extend([
i().addi(6, 0, LOADS).build(),
i().ld(7, data, 0).build(),
i().add(data, data, 7).build(),
i().addi(data, data, STRIDE).build(),
i().addi(6, 6, -1).build(),
i().bne(6, 0, -16).build(),
i().addi(DONE_REG as u32, 0, DONE as i32).build(),
i().jal(0, 0).build(),
]);
program
}
fn map_data(ctx: &mut TestContext, tables: &mut PageTables, data_va: u64, data: Data) {
let next_va = data_va + 0x1000;
match data {
Data::Megapage => tables.map(ctx, data_va, FIRST_FRAME, 1, PTE_LEAF_RWX_AD),
Data::Gigapage => tables.map(ctx, data_va, GIGAPAGE_FRAME, 2, PTE_LEAF_RWX_AD),
Data::TouchedSecondPage
| Data::UntouchedSecondPage
| Data::ExecuteOnlySecondPage
| Data::PmpDeniedSecondPage => {
tables.map(ctx, data_va, FIRST_FRAME, 0, PTE_LEAF_RWX_AD);
tables.map(ctx, next_va, SECOND_FRAME, 0, PTE_LEAF_RWX_AD);
}
}
}
struct Outcome {
held: Vec<LineAddr>,
page_boundary: f64,
tlb_miss: f64,
denied: f64,
}
impl Outcome {
fn holds(&self, paddr: u64) -> bool {
self.held.contains(&LineAddr::from_phys(PhysAddr::new(paddr), 64))
}
fn prefetched_into(&self, frame: u64) -> Vec<u64> {
(1..4).map(|line| frame + line * 64 * 4).filter(|&paddr| self.holds(paddr)).collect()
}
}
pub fn run_system(
backend: BackendKind,
mode: Mode,
page_boundary: PageBoundary,
data: Data,
) -> TestContext {
let mut config = Config::default();
config.pipeline.backend = backend;
config.cache.l1_d.enabled = true;
config.cache.l1_d.size_bytes = 32 * 1024;
config.cache.l1_d.ways = 8;
config.cache.l1_d.prefetcher = PrefetcherKind::None;
config.cache.load_prefetcher =
LoadPrefetcherConfig::Stride { table_size: 64, l1_lines: 4, l2_lines: 0, page_boundary };
config.system.console = crate::config::Console::Quiet;
let mut ctx = TestContext::new_with_config(&config).with_memory(RAM_SIZE, RAM_BASE);
let code = program(matches!(data, Data::TouchedSecondPage));
for (n, inst) in code.iter().enumerate() {
ctx.sim.probe_mem_store(PhysAddr::new(CODE + 4 * n as u64), u64::from(*inst), 4);
}
let data_va = mode.data_va();
let mut tables = PageTables::new(mode.levels());
tables.map(&mut ctx, CODE, CODE, 1, PTE_LEAF_RWX_AD);
map_data(&mut ctx, &mut tables, data_va, data);
ctx.set_reg(DATA_REG, data_va);
ctx.set_reg(NEXT_PAGE_REG, data_va + 0x1000);
{
let hart = &mut ctx.sim.state.harts[0];
hart.csrs.satp = (mode.satp_mode() << 60) | tables.root;
hart.privilege = PrivilegeMode::Supervisor;
if matches!(data, Data::PmpDeniedSecondPage) {
hart.pmp.set_addr(0, napot(SECOND_FRAME, 0x1000));
hart.pmp.set_cfg(0, PMP_NAPOT);
hart.pmp.set_addr(1, u64::MAX >> 10);
hart.pmp.set_cfg(1, PMP_NAPOT | PMP_RWX);
} else {
hart.pmp.set_addr(0, u64::MAX >> 10);
hart.pmp.set_cfg(0, 0b0000_1111);
}
hart.pc = CODE;
}
let second_page_in_tlb = match data {
Data::ExecuteOnlySecondPage => Some(PTE_LEAF_X_A),
Data::PmpDeniedSecondPage => Some(PTE_LEAF_RWX_AD),
_ => None,
};
if let Some(flags) = second_page_in_tlb {
let vpn = Vpn::new(((data_va + 0x1000) >> 12) & ((1 << 44) - 1));
let ppn = Ppn::new(SECOND_FRAME >> 12);
let dtlb = &mut ctx.sim.state.cores[0].units.mmu.dtlb;
dtlb.insert(vpn, ppn, flags | PTE_V, Asid::new(0), PageSize::Kib4);
}
ctx.sim.state.direct_mode = false;
ctx.sim.sync_arch_regs();
ctx.run_until(50_000, |ctx| ctx.get_reg(DONE_REG) == DONE).expect("the loop finished");
ctx.run(500);
ctx
}
fn run(backend: BackendKind, mode: Mode, page_boundary: PageBoundary, data: Data) -> Outcome {
let ctx = run_system(backend, mode, page_boundary, data);
let held = ctx.sim.state.cores[0].units.l1_d_cache.held_lines();
let stats = &ctx.sim.state.stats;
let stat =
|name: &str| stats.get(format!("core0.prefetch.loads.dropped.{name}")).unwrap_or(0.0);
Outcome {
held: held.into_iter().map(|(line, _)| line).collect(),
page_boundary: stat("page_boundary"),
tlb_miss: stat("tlb_miss"),
denied: stat("denied"),
}
}
fn for_every_machine(page_boundary: PageBoundary, data: Data, check: impl Fn(&Outcome, &str)) {
for backend in [BackendKind::InOrder, BackendKind::OutOfOrder] {
for mode in Mode::ALL {
let outcome = run(backend, mode, page_boundary, data);
check(&outcome, &format!("{backend:?} {mode:?}"));
}
}
}
const AFTER_FIRST_FRAME: u64 = FIRST_FRAME + 0x1000;
#[test]
fn crossing_with_the_tlb_prefetches_the_next_page_from_its_own_frame() {
for_every_machine(PageBoundary::CrossWithTlb, Data::TouchedSecondPage, |outcome, machine| {
assert!(!outcome.prefetched_into(SECOND_FRAME).is_empty(), "{machine}: nothing crossed");
assert!(!outcome.holds(AFTER_FIRST_FRAME), "{machine}");
assert!(outcome.prefetched_into(AFTER_FIRST_FRAME).is_empty(), "{machine}");
});
}
#[test]
fn crossing_drops_a_prefetch_whose_page_misses_the_tlb() {
for_every_machine(PageBoundary::CrossWithTlb, Data::UntouchedSecondPage, |outcome, machine| {
assert!(outcome.tlb_miss > 0.0, "{machine}");
assert!(!outcome.holds(SECOND_FRAME), "{machine}");
assert!(outcome.prefetched_into(SECOND_FRAME).is_empty(), "{machine}");
assert!(!outcome.holds(AFTER_FIRST_FRAME), "{machine}");
});
}
#[test]
fn crossing_drops_a_prefetch_into_a_page_the_load_may_not_read() {
for_every_machine(
PageBoundary::CrossWithTlb,
Data::ExecuteOnlySecondPage,
|outcome, machine| {
assert!(outcome.denied > 0.0, "{machine}");
assert!(!outcome.holds(SECOND_FRAME), "{machine}");
assert!(!outcome.holds(AFTER_FIRST_FRAME), "{machine}");
},
);
}
#[test]
fn crossing_drops_a_prefetch_into_a_frame_pmp_denies() {
for_every_machine(PageBoundary::CrossWithTlb, Data::PmpDeniedSecondPage, |outcome, machine| {
assert!(outcome.denied > 0.0, "{machine}");
assert!(!outcome.holds(SECOND_FRAME), "{machine}");
assert!(outcome.prefetched_into(SECOND_FRAME).is_empty(), "{machine}");
assert!(!outcome.holds(AFTER_FIRST_FRAME), "{machine}");
});
}
#[test]
fn stopping_keeps_prefetches_in_the_trained_page() {
for_every_machine(PageBoundary::Stop, Data::TouchedSecondPage, |outcome, machine| {
assert!(outcome.page_boundary > 0.0, "{machine}");
assert!(outcome.prefetched_into(SECOND_FRAME).is_empty(), "{machine}");
assert!(!outcome.holds(AFTER_FIRST_FRAME), "{machine}");
});
}
#[test]
fn stopping_inside_a_megapage_does_not_stop_at_4k() {
for_every_machine(PageBoundary::Stop, Data::Megapage, |outcome, machine| {
assert_eq!(outcome.page_boundary, 0.0, "{machine}");
let next_4k = Data::Megapage.first_frame() + 0x1000;
assert!(!outcome.prefetched_into(next_4k).is_empty(), "{machine}");
});
}
#[test]
fn stopping_inside_a_gigapage_does_not_stop_at_4k() {
for_every_machine(PageBoundary::Stop, Data::Gigapage, |outcome, machine| {
assert_eq!(outcome.page_boundary, 0.0, "{machine}");
let next_4k = Data::Gigapage.first_frame() + 0x1000;
assert!(!outcome.prefetched_into(next_4k).is_empty(), "{machine}");
});
}