use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use crate::core::space::{
AccessConstraints, AddressSpace, FlatTarget, MemAttrs, MemResult, Perms, RamStore,
};
use crate::core::value::{Endian, Width};
use crate::ir::AccessKind;
pub const PAGE_SIZE: u64 = 4096;
pub const PAGE_MASK: u64 = PAGE_SIZE - 1;
pub const DEFAULT_ENTRIES: u64 = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
pub struct Epoch {
pub topology: u64,
pub translation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
pub struct Context {
pub level: u8,
pub translating: bool,
}
impl Context {
#[inline]
const fn bits(self) -> u64 {
((self.level as u64 & 7) << 2) | ((self.translating as u64) << 1)
}
}
#[derive(Debug, Clone, Copy)]
struct Entry {
tag: u64,
addend: u64,
store: u32,
endian: Endian,
}
impl Entry {
const EMPTY: u64 = 0;
const VALID: u64 = 1;
const SLOW: u32 = u32::MAX;
const fn empty() -> Entry {
Entry {
tag: Entry::EMPTY,
addend: 0,
store: Entry::SLOW,
endian: Endian::Little,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TlbStats {
pub hits: u64,
pub misses: u64,
pub slow: u64,
pub split: u64,
pub fills: u64,
pub refused: u64,
pub flushes: u64,
}
#[derive(Debug)]
pub struct Tlb {
space: Arc<AddressSpace>,
sets: [Box<[Entry]>; 3],
mask: u64,
stores: Vec<Arc<RamStore>>,
epoch: Epoch,
stats: TlbStats,
}
const MAX_STORES: usize = 64;
impl Tlb {
#[must_use]
pub fn new(space: Arc<AddressSpace>) -> Tlb {
Tlb::with_entries(space, DEFAULT_ENTRIES)
}
#[must_use]
pub fn with_entries(space: Arc<AddressSpace>, entries: u64) -> Tlb {
let entries = entries.max(1).next_power_of_two();
let n = usize::try_from(entries).unwrap_or(usize::MAX);
let epoch = Epoch {
topology: space.generation(),
translation: 0,
};
Tlb {
space,
sets: [
vec![Entry::empty(); n].into_boxed_slice(),
vec![Entry::empty(); n].into_boxed_slice(),
vec![Entry::empty(); n].into_boxed_slice(),
],
mask: entries - 1,
stores: Vec::new(),
epoch,
stats: TlbStats::default(),
}
}
#[inline]
#[must_use]
pub fn space(&self) -> &Arc<AddressSpace> {
&self.space
}
#[inline]
#[must_use]
pub fn stats(&self) -> TlbStats {
self.stats
}
#[inline]
#[must_use]
pub fn epoch(&self) -> Epoch {
self.epoch
}
pub fn sync(&mut self, epoch: Epoch) -> bool {
if self.epoch == epoch {
return false;
}
self.epoch = epoch;
self.flush();
true
}
#[inline]
#[must_use]
pub fn topology_generation(&self) -> u64 {
self.space.generation()
}
pub fn flush(&mut self) {
for set in &mut self.sets {
set.fill(Entry::empty());
}
self.stores.clear();
self.stats.flushes += 1;
}
pub fn invalidate_page(&mut self, addr: u64) {
let index = self.index(addr);
for set in &mut self.sets {
set[index] = Entry::empty();
}
}
pub fn read(
&mut self,
kind: AccessKind,
addr: u64,
phys: u64,
width: Width,
ctx: Context,
attrs: MemAttrs,
) -> MemResult<u64> {
let n = width.bytes() as usize;
if matches!(kind, AccessKind::Store) {
return self.space.read(phys, width, attrs);
}
if !within_page(addr, width) {
self.stats.split += 1;
return self.space.read(phys, width, attrs);
}
match self.probe(kind, addr, ctx) {
Probe::Ram {
store,
offset,
endian,
} => {
self.stats.hits += 1;
let mut buf = [0u8; 8];
self.stores[store].read_at(offset, &mut buf[..n])?;
endian.load(&buf[..n], width)
}
Probe::Slow => {
self.stats.slow += 1;
self.space.read(phys, width, attrs)
}
Probe::Miss => {
self.stats.misses += 1;
self.fill(kind, addr, phys, ctx);
self.space.read(phys, width, attrs)
}
}
}
pub fn write(
&mut self,
addr: u64,
phys: u64,
width: Width,
value: u64,
ctx: Context,
attrs: MemAttrs,
) -> MemResult {
let n = width.bytes() as usize;
if !within_page(addr, width) {
self.stats.split += 1;
return self.space.write(phys, width, value, attrs);
}
match self.probe(AccessKind::Store, addr, ctx) {
Probe::Ram {
store,
offset,
endian,
} => {
self.stats.hits += 1;
let mut buf = [0u8; 8];
endian.store(&mut buf[..n], width, value)?;
self.stores[store].write_at(offset, &buf[..n])
}
Probe::Slow => {
self.stats.slow += 1;
self.space.write(phys, width, value, attrs)
}
Probe::Miss => {
self.stats.misses += 1;
self.fill(AccessKind::Store, addr, phys, ctx);
self.space.write(phys, width, value, attrs)
}
}
}
pub fn fill(&mut self, kind: AccessKind, addr: u64, phys: u64, ctx: Context) {
let index = self.index(addr);
let tag = tag(addr, ctx);
let entry = self.resolve(kind, addr, phys);
if entry.store == Entry::SLOW {
self.stats.refused += 1;
}
self.stats.fills += 1;
self.sets[set_of(kind)][index] = Entry { tag, ..entry };
}
fn resolve(&mut self, kind: AccessKind, addr: u64, phys: u64) -> Entry {
let slow = Entry::empty();
let Some((store, offset, endian)) = self.probe_space(kind, addr, phys) else {
return slow;
};
let Some(index) = self.intern(&store) else {
return slow;
};
Entry {
tag: Entry::EMPTY,
addend: offset.wrapping_sub(addr & !PAGE_MASK),
store: index,
endian,
}
}
fn probe_space(
&self,
kind: AccessKind,
addr: u64,
phys: u64,
) -> Option<(Arc<RamStore>, u64, Endian)> {
if addr & PAGE_MASK != phys & PAGE_MASK {
return None;
}
let page = phys & !PAGE_MASK;
let view = self.space.try_view()?;
let entry = view.flat_view().entry(view.locate(page)?)?;
if entry.start() > page || entry.end() < page.checked_add(PAGE_SIZE)? {
return None;
}
if !entry.is_direct_ram() {
return None;
}
let leaf = entry.leaf()?;
if leaf.is_rebasable() || leaf.period().is_some() {
return None;
}
let need = match kind {
AccessKind::Fetch | AccessKind::Load => Perms::READ,
AccessKind::Store => Perms::WRITE,
};
if !leaf.perms().contains(need) || !permissive(leaf.constraints()) {
return None;
}
let FlatTarget::Ram(store) = leaf.target() else {
return None;
};
Some((
Arc::clone(store),
leaf.offset_of(page - entry.start()),
entry.endian(),
))
}
fn intern(&mut self, store: &Arc<RamStore>) -> Option<u32> {
if let Some(i) = self.stores.iter().position(|s| Arc::ptr_eq(s, store)) {
return u32::try_from(i).ok();
}
if self.stores.len() >= MAX_STORES {
return None;
}
self.stores.push(Arc::clone(store));
u32::try_from(self.stores.len() - 1).ok()
}
#[inline]
fn probe(&self, kind: AccessKind, addr: u64, ctx: Context) -> Probe {
let entry = &self.sets[set_of(kind)][self.index(addr)];
if entry.tag != tag(addr, ctx) {
return Probe::Miss;
}
if entry.store == Entry::SLOW {
return Probe::Slow;
}
Probe::Ram {
store: entry.store as usize,
offset: addr.wrapping_add(entry.addend),
endian: entry.endian,
}
}
#[inline]
fn index(&self, addr: u64) -> usize {
((addr >> 12) & self.mask) as usize
}
}
enum Probe {
Ram {
store: usize,
offset: u64,
endian: Endian,
},
Slow,
Miss,
}
#[inline]
const fn set_of(kind: AccessKind) -> usize {
match kind {
AccessKind::Fetch => 0,
AccessKind::Load => 1,
AccessKind::Store => 2,
}
}
#[inline]
const fn tag(addr: u64, ctx: Context) -> u64 {
(addr & !PAGE_MASK) | ctx.bits() | Entry::VALID
}
#[inline]
const fn within_page(addr: u64, width: Width) -> bool {
(addr & PAGE_MASK) + width.bytes() <= PAGE_SIZE
}
#[inline]
fn permissive(c: AccessConstraints) -> bool {
c.min == Width::U8
&& c.max == Width::U64
&& !c.natural_alignment
&& !c.secure_only
&& !c.privileged_only
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::error::BusError;
use crate::core::space::{Region, RomStore, UnassignedPolicy};
use alloc::sync::Arc;
const BASE: u64 = 0x2000_0000;
const SIZE: u64 = 8 * PAGE_SIZE;
fn space() -> (Arc<AddressSpace>, Arc<RamStore>) {
let ram = Arc::new(RamStore::new(SIZE));
let space = AddressSpace::new("mem", 64).with_unassigned(UnassignedPolicy::FAULT);
space
.topology()
.map(Region::ram("ram", Arc::clone(&ram)), BASE)
.expect("one region maps");
(Arc::new(space), ram)
}
fn tlb() -> (Tlb, Arc<RamStore>) {
let (space, ram) = space();
(Tlb::with_entries(space, 64), ram)
}
const BARE: Context = Context {
level: 3,
translating: false,
};
#[test]
fn a_hit_returns_what_the_address_space_would_have_returned() {
let (mut tlb, ram) = tlb();
ram.write_at(0x40, &0x1122_3344_5566_7788u64.to_le_bytes())
.expect("in range");
let addr = BASE + 0x40;
let first = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U64,
BARE,
MemAttrs::DEFAULT,
);
assert_eq!(first, Ok(0x1122_3344_5566_7788));
assert_eq!(tlb.stats().misses, 1);
let second = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U64,
BARE,
MemAttrs::DEFAULT,
);
assert_eq!(second, first);
assert_eq!(tlb.stats().hits, 1);
assert_eq!(
second,
tlb.space().read(addr, Width::U64, MemAttrs::DEFAULT)
);
}
#[test]
fn a_write_through_the_fast_path_marks_the_store_dirty() {
let (mut tlb, ram) = tlb();
let addr = BASE + 0x1004;
for _ in 0..2 {
tlb.write(addr, addr, Width::U32, 0xdead_beef, BARE, MemAttrs::DEFAULT)
.expect("the write lands");
}
assert_eq!(tlb.stats().hits, 1, "the second write hit");
assert_eq!(ram.read_u8(0x1004), Ok(0xef));
assert!(ram.is_page_dirty(0x1004 / ram.page_size()));
}
#[test]
fn loads_and_stores_do_not_share_a_set() {
let (mut tlb, _ram) = tlb();
let addr = BASE + 0x2000;
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 1);
let before = tlb.stats().misses;
tlb.write(addr, addr, Width::U8, 1, BARE, MemAttrs::DEFAULT)
.expect("the write lands");
assert_eq!(tlb.stats().misses, before + 1);
}
#[test]
fn a_different_privilege_level_is_a_different_entry() {
let (mut tlb, _ram) = tlb();
let addr = BASE + 0x3000;
let user = Context {
level: 0,
translating: true,
};
let supervisor = Context {
level: 1,
translating: true,
};
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
user,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 1);
let before = tlb.stats().misses;
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
supervisor,
MemAttrs::DEFAULT,
);
assert_eq!(
tlb.stats().misses,
before + 1,
"a supervisor access must not hit a user entry"
);
}
#[test]
fn a_bare_access_never_hits_a_translated_entry_at_the_same_number() {
let (mut tlb, _ram) = tlb();
let addr = BASE + 0x4000;
let paged = Context {
level: 1,
translating: true,
};
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
paged,
MemAttrs::DEFAULT,
);
}
let before = tlb.stats().misses;
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
assert_eq!(tlb.stats().misses, before + 1);
}
#[test]
fn an_access_that_spans_two_pages_is_never_served_from_an_entry() {
let (mut tlb, ram) = tlb();
ram.write_at(PAGE_SIZE - 4, &[1, 2, 3, 4, 5, 6, 7, 8])
.expect("in range");
let addr = BASE + PAGE_SIZE - 4;
let want = tlb.space().read(addr, Width::U64, MemAttrs::DEFAULT);
for _ in 0..4 {
assert_eq!(
tlb.read(
AccessKind::Load,
addr,
addr,
Width::U64,
BARE,
MemAttrs::DEFAULT
),
want
);
}
assert_eq!(tlb.stats().hits, 0);
assert_eq!(tlb.stats().split, 4);
}
#[test]
fn a_rom_page_is_remembered_as_uncacheable_rather_than_reprobed() {
let (space, _ram) = space();
let rom = Arc::new(RomStore::zeroed(PAGE_SIZE));
space
.topology()
.map(
Region::rom("rom", rom, crate::core::space::RomWrite::Ignore),
BASE + SIZE,
)
.expect("the rom maps");
let mut tlb = Tlb::with_entries(Arc::clone(&space), 64);
let addr = BASE + SIZE;
for _ in 0..3 {
assert_eq!(
tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT
),
Ok(0)
);
}
assert_eq!(tlb.stats().misses, 1, "the flat view is probed once");
assert_eq!(tlb.stats().slow, 2, "and then remembered as uncacheable");
assert_eq!(tlb.stats().refused, 1);
assert_eq!(tlb.stats().hits, 0);
}
#[test]
fn a_rebasable_window_is_never_cached() {
let (space, ram) = space();
let window = Region::alias(
"bank",
Region::ram("backing", Arc::clone(&ram)),
0,
PAGE_SIZE,
)
.expect("an alias over the ram");
space
.topology()
.map(window, BASE + SIZE)
.expect("the window maps");
let mut tlb = Tlb::with_entries(Arc::clone(&space), 64);
let addr = BASE + SIZE;
for _ in 0..3 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 0, "a rebasable leaf is never cached");
assert_eq!(tlb.stats().refused, 1);
}
#[test]
fn a_read_only_mapping_is_not_cached_for_stores() {
let (space, ram) = space();
space
.topology()
.map_with_perms(
Region::ram("ro", Arc::clone(&ram)),
BASE + SIZE,
Perms::READ,
)
.expect("the read-only view maps");
let mut tlb = Tlb::with_entries(Arc::clone(&space), 64);
let addr = BASE + SIZE;
for _ in 0..3 {
assert_eq!(
tlb.write(addr, addr, Width::U8, 0xaa, BARE, MemAttrs::DEFAULT),
Err(BusError::Protected),
"the fast path must refuse exactly where the slow path does"
);
}
assert_eq!(tlb.stats().hits, 0);
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 1);
}
#[test]
fn a_write_only_mapping_never_answers_a_read_from_its_store_entry() {
let (space, ram) = space();
space
.topology()
.map_with_perms(
Region::ram("wo", Arc::clone(&ram)),
BASE + SIZE,
Perms::WRITE,
)
.expect("the write-only view maps");
let mut tlb = Tlb::with_entries(Arc::clone(&space), 64);
let addr = BASE + SIZE;
for _ in 0..2 {
tlb.write(addr, addr, Width::U8, 0x5a, BARE, MemAttrs::DEFAULT)
.expect("the write lands");
}
assert_eq!(tlb.stats().hits, 1, "the store side is cached");
for kind in [AccessKind::Load, AccessKind::Fetch, AccessKind::Store] {
assert_eq!(
tlb.read(kind, addr, addr, Width::U8, BARE, MemAttrs::DEFAULT),
Err(BusError::Protected),
"a {kind:?} through a write-only mapping must be refused"
);
}
}
#[test]
fn a_topology_change_invalidates_every_entry() {
let (mut tlb, _ram) = tlb();
let addr = BASE + 0x5000;
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 1);
let ram2 = Arc::new(RamStore::new(PAGE_SIZE));
tlb.space()
.topology()
.map(Region::ram("other", ram2), BASE + 0x10_0000)
.expect("a second region maps");
let epoch = Epoch {
topology: tlb.topology_generation(),
translation: 0,
};
assert!(tlb.sync(epoch), "the generation moved");
let before = tlb.stats().misses;
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
assert_eq!(tlb.stats().misses, before + 1);
}
#[test]
fn a_translation_generation_bump_invalidates_every_entry() {
let (mut tlb, _ram) = tlb();
let paged = Context {
level: 1,
translating: true,
};
let addr = BASE + 0x6000;
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
paged,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 1);
let mut epoch = tlb.epoch();
epoch.translation += 1;
assert!(tlb.sync(epoch));
let before = tlb.stats().misses;
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
paged,
MemAttrs::DEFAULT,
);
assert_eq!(tlb.stats().misses, before + 1);
}
#[test]
fn one_page_can_be_invalidated_without_the_rest() {
let (mut tlb, _ram) = tlb();
let a = BASE;
let b = BASE + PAGE_SIZE;
for addr in [a, b, a, b] {
let _ = tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 2);
tlb.invalidate_page(a);
let before = tlb.stats();
let _ = tlb.read(AccessKind::Load, b, b, Width::U8, BARE, MemAttrs::DEFAULT);
assert_eq!(tlb.stats().hits, before.hits + 1, "b survived");
let before = tlb.stats();
let _ = tlb.read(AccessKind::Load, a, a, Width::U8, BARE, MemAttrs::DEFAULT);
assert_eq!(tlb.stats().misses, before.misses + 1, "a did not");
}
#[test]
fn the_fast_path_and_the_slow_path_agree_on_every_width_and_offset() {
let (mut tlb, ram) = tlb();
for i in 0..256u64 {
ram.write_u8(i, (i * 7) as u8).expect("in range");
}
for width in [Width::U8, Width::U16, Width::U32, Width::U64] {
for off in 0..64u64 {
let addr = BASE + off;
let want = tlb.space().read(addr, width, MemAttrs::DEFAULT);
for _ in 0..2 {
assert_eq!(
tlb.read(AccessKind::Load, addr, addr, width, BARE, MemAttrs::DEFAULT),
want,
"width {width:?} at {addr:#x}"
);
}
}
}
assert!(tlb.stats().hits > 0);
}
#[test]
fn an_unmapped_page_faults_the_same_way_twice() {
let (mut tlb, _ram) = tlb();
let addr = BASE + 0x100_0000;
for _ in 0..3 {
assert_eq!(
tlb.read(
AccessKind::Load,
addr,
addr,
Width::U8,
BARE,
MemAttrs::DEFAULT
),
Err(BusError::Unassigned)
);
}
}
#[test]
fn a_page_the_region_only_half_covers_is_not_cached() {
let space = AddressSpace::new("mem", 64).with_unassigned(UnassignedPolicy::FAULT);
let ram = Arc::new(RamStore::new(PAGE_SIZE / 2));
space
.topology()
.map(Region::ram("half", ram), BASE)
.expect("the half page maps");
let mut tlb = Tlb::with_entries(Arc::new(space), 64);
for _ in 0..2 {
let _ = tlb.read(
AccessKind::Load,
BASE,
BASE,
Width::U8,
BARE,
MemAttrs::DEFAULT,
);
}
assert_eq!(tlb.stats().hits, 0);
assert_eq!(
tlb.read(
AccessKind::Load,
BASE + PAGE_SIZE - 1,
BASE + PAGE_SIZE - 1,
Width::U8,
BARE,
MemAttrs::DEFAULT
),
Err(BusError::Unassigned)
);
}
}