rustyn64_cpu/tlb.rs
1//! The joint TLB and the instruction micro-TLB (T-12-004).
2//!
3//! 32 fully-associative joint-TLB (JTLB) entries, each mapping an **even/odd
4//! page pair**, plus a two-entry instruction micro-TLB (ITLB) in front of it.
5//!
6//! # The distinction that is easy to lose
7//!
8//! A **micro-TLB miss is a stall** (3 `PCycles`, UM §4.6.2 p. 107); a **JTLB miss
9//! is an exception**. An implementation with only the JTLB does not approximate
10//! the micro-TLB's cost — it deletes the structure the cost occurs in, so there
11//! is nowhere left to charge it.
12//!
13//! # The matching rule, and the trap in it
14//!
15//! An entry matches when `VPN2` matches **and** (`G` is set **or** the `ASID`
16//! matches). The **`V` bit does not participate** (UM §5.4.9, p. 155):
17//!
18//! > *"While the V bit of the entry must be set for a valid translation to take
19//! > place, it is not involved in the determination of a matching TLB entry."*
20//!
21//! So an invalid entry still *matches* — it just raises TLB Invalid instead of
22//! translating. Checking `V` during matching looks like an optimization, passes
23//! ordinary tests, and breaks two things: an invalid entry would fall through to
24//! a **refill** (wrong vector, wrong handler), and TLB shutdown would stop
25//! firing on duplicates involving an invalid entry, which UM Fig. 6-6 (p. 167)
26//! explicitly says it must.
27
28use crate::cop0::{Cop0, reg};
29use serde::{Deserialize, Serialize};
30
31/// The bits of a virtual address that `EntryHi.VPN2` can hold: VA(39:0).
32///
33/// Everything above is either the `R` region field or sign extension, neither of
34/// which belongs in a `VPN2` comparison.
35pub const VA_MASK: u64 = 0x0000_00FF_FFFF_FFFF;
36
37/// The `EntryHi.VPN2` field mask.
38pub const VPN2_MASK: u64 = 0x0000_00FF_FFFF_E000;
39
40/// The `EntryHi.ASID` field mask.
41pub const ASID_MASK: u64 = 0xFF;
42
43/// JTLB entries. Fully associative (UM §5.1, p. 122).
44pub const JTLB_ENTRIES: usize = 32;
45
46/// Instruction micro-TLB entries (UM §1.5.1).
47pub const ITLB_ENTRIES: usize = 2;
48
49/// The micro-TLB reload penalty, in `PCycle`s (UM §4.6.2, p. 107).
50///
51/// *"A miss penalty of 3 `PCycles` is incurred when the micro-TLB is updated from
52/// the JTLB."* Documented, not fitted — accuracy-ledger C-7.
53pub const ITLB_MISS_PCYCLES: u32 = 3;
54
55/// One JTLB entry: a `VPN2` tag plus the even and odd page it maps.
56#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
57pub struct Entry {
58 /// `PageMask`, selecting the page size.
59 pub page_mask: u32,
60 /// The pair's tag: `VA(39:13)` with every bit `PageMask` covers cleared,
61 /// held **in place** rather than divided down.
62 ///
63 /// Storing the masked address rather than `address / pair_size` matters
64 /// because `PageMask` need not be contiguous: a canonicalized mask can have
65 /// a hole (`0b11_11_11_11_00` covers bits 22:15 and leaves 14:13 alone), and
66 /// division only ever clears the *low* bits. The two agree for every legal
67 /// page size and diverge exactly on the masks n64-systemtest writes to check
68 /// the read-back.
69 pub vpn2: u64,
70 /// Address-space identifier.
71 pub asid: u8,
72 /// Global: ignore `ASID` when matching.
73 ///
74 /// Derived on write as `EntryLo0.G AND EntryLo1.G` (UM Fig. 5-10, p. 145),
75 /// which is why it lives on the entry rather than per-page.
76 pub global: bool,
77 /// The `R` field (bits 63:62 of `EntryHi`) — the 64-bit address region.
78 pub region: u8,
79 /// Even page: `EntryLo0`.
80 pub lo0: PageEntry,
81 /// Odd page: `EntryLo1`.
82 pub lo1: PageEntry,
83}
84
85/// One half of an entry — the even or odd page.
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
87pub struct PageEntry {
88 /// Page frame number.
89 pub pfn: u32,
90 /// Cache coherency attribute. **Only `2` means uncached** (UM Table 5-6,
91 /// p. 145); 0, 1, 3, 4, 5, 6 and 7 are all cached, because the VR4300 has no
92 /// coherency protocol and the VR4400's finer encodings collapse.
93 pub c: u8,
94 /// Dirty — meaning **writable**, not "has been written". A store to a page
95 /// with `D` clear raises TLB Modified.
96 pub dirty: bool,
97 /// Valid. Does **not** participate in matching; see the module docs.
98 pub valid: bool,
99}
100
101impl PageEntry {
102 /// Is this page uncached?
103 #[must_use]
104 pub const fn uncached(self) -> bool {
105 self.c == 2
106 }
107}
108
109/// Why a translation failed.
110#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
111pub enum TlbFault {
112 /// No entry matched — TLB refill (`TLBL`/`TLBS`, refill vector).
113 Refill,
114 /// An entry matched but its `V` bit is clear — TLB Invalid (`TLBL`/`TLBS`,
115 /// **general** vector).
116 Invalid,
117 /// A store to a matching, valid page whose `D` bit is clear — TLB Modified.
118 Modified,
119}
120
121/// A successful translation.
122#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
123pub struct Translated {
124 /// The physical address.
125 pub addr: u32,
126 /// Whether the access bypasses the caches.
127 pub uncached: bool,
128}
129
130/// The joint TLB plus its instruction micro-TLB.
131#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
132pub struct Tlb {
133 /// The 32 joint entries.
134 entries: [Entry; JTLB_ENTRIES],
135 /// Micro-ITLB: indices into `entries`, plus an LRU bit.
136 ///
137 /// Holds *indices* rather than copies so a `TLBWI` cannot leave the ITLB
138 /// serving a stale mapping — a real hazard on hardware that software must
139 /// handle, but not one worth reproducing by accident.
140 itlb: [Option<usize>; ITLB_ENTRIES],
141 /// Which ITLB way is least recently used (UM §1.5.1 specifies LRU).
142 itlb_lru: usize,
143 /// TLB shutdown: two or more entries matched, and the TLB is now unusable
144 /// until reset (UM §5.1, p. 122).
145 shutdown: bool,
146}
147
148impl Default for Tlb {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl Tlb {
155 /// A TLB in its power-on state.
156 ///
157 /// The manual calls the reset contents **undefined** (UM §6.4.4, p. 183) and
158 /// ADR 0004 requires reproducibility, so a fixed state must be chosen — but
159 /// **all-zero is not a usable choice**, and that is not obvious.
160 ///
161 /// Zeroing gives all 32 entries `VPN2 = 0` and `ASID = 0`. Since `V` does
162 /// not participate in matching, *any* access to virtual page-pair 0 then
163 /// matches all 32 entries at once, which is the TLB-shutdown condition — so
164 /// the very first KUSEG access to low memory would brick the TLB.
165 ///
166 /// Each entry therefore gets a **distinct** `VPN2` near the top of the
167 /// field, so no two coincide and none sits where software is likely to look.
168 /// Recorded as accuracy-ledger **D-4**: a deliberate deviation, chosen
169 /// because real hardware powers up with *arbitrary* contents that do not
170 /// coincide, and zero is the one arbitrary value that does.
171 #[must_use]
172 pub const fn new() -> Self {
173 let mut t = Self {
174 entries: [Entry {
175 page_mask: 0,
176 vpn2: 0,
177 asid: 0,
178 global: false,
179 region: 0,
180 lo0: PageEntry {
181 pfn: 0,
182 c: 0,
183 dirty: false,
184 valid: false,
185 },
186 lo1: PageEntry {
187 pfn: 0,
188 c: 0,
189 dirty: false,
190 valid: false,
191 },
192 }; JTLB_ENTRIES],
193 itlb: [None; ITLB_ENTRIES],
194 itlb_lru: 0,
195 shutdown: false,
196 };
197 // Distinct, non-coinciding tags. `VPN2` is 27 bits, so counting down
198 // from its maximum keeps them clear of anything a program maps.
199 let mut i = 0;
200 while i < JTLB_ENTRIES {
201 t.entries[i].vpn2 = ((0x7FF_FFFF - i) as u64) << 13;
202 i += 1;
203 }
204 t
205 }
206
207 /// Has the TLB shut down? *"the processor must be reset to restart"*
208 /// (UM Fig. 6-6, p. 167).
209 #[must_use]
210 pub const fn is_shutdown(&self) -> bool {
211 self.shutdown
212 }
213
214 /// Read an entry (for `TLBR` and for tests).
215 #[must_use]
216 pub const fn entry(&self, i: usize) -> Entry {
217 self.entries[i & (JTLB_ENTRIES - 1)]
218 }
219
220 /// The page size an entry maps, in bytes, from its `PageMask`.
221 ///
222 /// `PageMask` bits 24:13 select 4K…16M (UM Table 5-7, p. 149). *"When the
223 /// Mask field is not one of the values shown in Table 5-7, the operation of
224 /// the TLB is undefined"* — we take the mask at face value, which is a
225 /// documented-undefined choice rather than a hardware fact.
226 #[must_use]
227 pub const fn page_size(mask: u32) -> u64 {
228 // `(mask | 0x1FFF) + 1` is the size of the PAIR; one page is half that.
229 // Mask 0 gives a 0x2000 pair and a 0x1000 page, which is the 4 KiB row
230 // of Table 5-7. Returning the pair size here is an easy off-by-one-bit
231 // that makes every entry cover twice its range and match twice as often.
232 Self::pair_size(mask) >> 1
233 }
234
235 /// The size of the even/odd **pair** an entry covers — twice
236 /// [`Tlb::page_size`], and the granularity the `VPN2` tag is compared at.
237 #[must_use]
238 pub const fn pair_size(mask: u32) -> u64 {
239 ((mask as u64) | 0x1FFF) + 1
240 }
241
242 /// Look up a virtual address.
243 ///
244 /// `store` selects the `D`-bit check; `asid` is the current `EntryHi.ASID`.
245 ///
246 /// # Errors
247 ///
248 /// [`TlbFault`] describing which of the three TLB exceptions to raise.
249 pub fn lookup(&mut self, vaddr: u64, asid: u8, store: bool) -> Result<Translated, TlbFault> {
250 // "the TLB cannot be used" and "the processor must be reset to restart"
251 // (UM §5.1 p. 122, Fig. 6-6 p. 167). Without this the flag would be
252 // recorded and then ignored, which is worse than not having it.
253 if self.shutdown {
254 return Err(TlbFault::Refill);
255 }
256 let mut found: Option<usize> = None;
257 for (i, e) in self.entries.iter().enumerate() {
258 if !Self::matches(e, vaddr, asid) {
259 continue;
260 }
261 if found.is_some() {
262 // "If there are two or more TLB entries that coincide, the TLB
263 // operation is not correctly executed. In this case, the
264 // TLB-Shutdown (TS) bit of the status register is set to 1, and
265 // then the TLB cannot be used" (UM §5.1, p. 122).
266 //
267 // Reachable with an INVALID duplicate, because V is not part of
268 // matching -- UM Fig. 6-6 says so explicitly.
269 self.shutdown = true;
270 return Err(TlbFault::Refill);
271 }
272 found = Some(i);
273 }
274 let i = found.ok_or(TlbFault::Refill)?;
275 let e = &self.entries[i];
276
277 // Which half of the pair? The bit just above the page-size field.
278 let size = Self::page_size(e.page_mask);
279 let page = if (vaddr & size) == 0 { e.lo0 } else { e.lo1 };
280
281 if !page.valid {
282 // Matched but invalid: TLB Invalid, which takes the GENERAL vector,
283 // not the refill vector. Treating it as a miss would send the
284 // handler to the wrong place.
285 return Err(TlbFault::Invalid);
286 }
287 if store && !page.dirty {
288 // "Dirty" means WRITABLE here, not "has been written".
289 return Err(TlbFault::Modified);
290 }
291 // PFN is always in **4 KiB units**, whatever the page size — so a large
292 // page's frame number has low bits that must be masked off rather than
293 // scaled. Multiplying by `size` instead would place a 16 KiB page four
294 // times too high in physical memory.
295 let offset = vaddr & (size - 1);
296 let base = ((page.pfn as u64) << 12) & !(size - 1);
297 Ok(Translated {
298 addr: (base | offset) as u32,
299 uncached: page.uncached(),
300 })
301 }
302
303 /// Does this entry match?
304 ///
305 /// `VPN2` **and** (`G` **or** `ASID`). `V` is deliberately absent — see the
306 /// module docs for why including it breaks two separate behaviors.
307 fn matches(e: &Entry, vaddr: u64, asid: u8) -> bool {
308 // Compare the ARCHITECTURAL fields, not the raw 64-bit value.
309 //
310 // A 32-bit kernel address arrives sign-extended -- KSEG3 is
311 // `0xFFFF_FFFF_E000_0000`, not `0xE000_0000` -- while `EntryHi.VPN2`
312 // holds only VA(39:13). Dividing the raw value would compare the sign
313 // extension against zero, so **no mapped kernel address would ever
314 // match**, however correct the entry. KUSEG addresses have no sign
315 // extension, which is why a test suite built on them sees nothing wrong.
316 if Self::region_of(vaddr) != e.region {
317 return false;
318 }
319 if Self::vpn2_of(vaddr, e.page_mask) != e.vpn2 {
320 return false;
321 }
322 e.global || e.asid == asid
323 }
324
325 /// The `R` field: VA(63:62), selecting the 64-bit address region.
326 const fn region_of(vaddr: u64) -> u8 {
327 ((vaddr >> 62) & 0b11) as u8
328 }
329
330 /// `VPN2` for an address: VA(39:13) with the `PageMask` bits cleared.
331 ///
332 /// Masked to 40 bits **first**, so the sign extension of a 32-bit address is
333 /// discarded rather than compared.
334 const fn vpn2_of(vaddr: u64, page_mask: u32) -> u64 {
335 (vaddr & VA_MASK) & !Self::offset_mask(page_mask)
336 }
337
338 /// The bits a page of this size treats as offset: `PageMask` plus 12:0.
339 const fn offset_mask(page_mask: u32) -> u64 {
340 page_mask as u64 | 0x1FFF
341 }
342
343 /// `TLBWI` / `TLBWR` — write `EntryHi`/`EntryLo0`/`EntryLo1`/`PageMask` into
344 /// entry `index`.
345 pub fn write_entry(&mut self, index: usize, cop0: &Cop0) {
346 let hi = cop0.read(reg::ENTRY_HI);
347 let lo0 = cop0.read(reg::ENTRY_LO0);
348 let lo1 = cop0.read(reg::ENTRY_LO1);
349 let mask = Self::canonical_page_mask(cop0.read(reg::PAGE_MASK) as u32);
350
351 self.entries[index & (JTLB_ENTRIES - 1)] = Entry {
352 page_mask: mask,
353 vpn2: (hi & VPN2_MASK) & !Self::offset_mask(mask),
354 asid: (hi & ASID_MASK) as u8,
355 // "If this bit is set in BOTH EntryLo0 and EntryLo1, then the
356 // processor ignores the ASID during TLB lookup" (UM Fig. 5-10,
357 // p. 145). An OR here would make far too many entries global.
358 global: (lo0 & 1) != 0 && (lo1 & 1) != 0,
359 region: ((hi >> 62) & 0b11) as u8,
360 lo0: Self::page_from(lo0),
361 lo1: Self::page_from(lo1),
362 };
363 // A write invalidates the micro-TLB, which caches indices into this
364 // array. Cheaper and safer than tracking which way held `index`.
365 self.itlb = [None; ITLB_ENTRIES];
366 }
367
368 /// Canonicalize a written `PageMask` to what the entry actually stores.
369 ///
370 /// `PageMask` bits 24:13 are **six 2-bit pairs**, and an entry does not store
371 /// twelve independent bits: each pair reads back as `11` exactly when its
372 /// **higher** bit was written, and as `00` otherwise. A written `0b01` pair
373 /// is discarded; a written `0b10` pair becomes `0b11`.
374 ///
375 /// So the natural implementation — store the value, mask it to 24:13 — is
376 /// wrong in both directions, and quietly: it accepts page sizes the hardware
377 /// has no encoding for, and it reports back a mask that was never stored.
378 /// n64-systemtest writes seventeen values and checks each read-back
379 /// (`tests/tlb/mod.rs`), including `0b00000000100` → `0`, where a masking
380 /// implementation returns the input unchanged.
381 ///
382 /// Applied on **write**, because that is where the information is lost —
383 /// the entry has nowhere to keep the discarded bits.
384 const fn canonical_page_mask(mask: u32) -> u32 {
385 let mut out = 0u32;
386 let mut pair = 0;
387 while pair < 6 {
388 let high = 13 + 2 * pair + 1;
389 if (mask >> high) & 1 != 0 {
390 out |= 0b11 << (13 + 2 * pair);
391 }
392 pair += 1;
393 }
394 out
395 }
396
397 /// Decode an `EntryLo` into a page.
398 const fn page_from(lo: u64) -> PageEntry {
399 PageEntry {
400 pfn: ((lo >> 6) & 0x000F_FFFF) as u32,
401 c: ((lo >> 3) & 0b111) as u8,
402 dirty: (lo >> 2) & 1 != 0,
403 valid: (lo >> 1) & 1 != 0,
404 }
405 }
406
407 /// `TLBR` — read entry `index` back into the COP0 registers.
408 pub fn read_entry(&self, index: usize, cop0: &mut Cop0) {
409 let e = self.entries[index & (JTLB_ENTRIES - 1)];
410 cop0.set_hardware(reg::PAGE_MASK, u64::from(e.page_mask));
411 cop0.set_hardware(
412 reg::ENTRY_HI,
413 ((e.region as u64) << 62) | e.vpn2 | u64::from(e.asid),
414 );
415 // `EntryHi` has no G field, so the entry's G is written back into BOTH
416 // EntryLo halves -- which is the inverse of the AND applied on write.
417 cop0.set_hardware(reg::ENTRY_LO0, Self::lo_from(e.lo0, e.global));
418 cop0.set_hardware(reg::ENTRY_LO1, Self::lo_from(e.lo1, e.global));
419 }
420
421 /// Encode a page back into an `EntryLo` value.
422 const fn lo_from(p: PageEntry, global: bool) -> u64 {
423 ((p.pfn as u64) << 6)
424 | ((p.c as u64) << 3)
425 | ((p.dirty as u64) << 2)
426 | ((p.valid as u64) << 1)
427 | global as u64
428 }
429
430 /// `TLBP` — probe for an entry matching the current `EntryHi`.
431 ///
432 /// Sets `Index` to the matching index, or sets `Index.P` (bit 31) on a miss:
433 /// *"Set to 1 when the previous `TLBProbe` (`TLBP`) instruction was
434 /// unsuccessful"* (UM §5.4.1, p. 146).
435 ///
436 /// **What the low bits hold on a miss is undocumented** (accuracy-ledger
437 /// U-2). This implementation leaves them zero — a guess, not a fact.
438 pub fn probe(&self, cop0: &mut Cop0) {
439 let hi = cop0.read(reg::ENTRY_HI);
440 let asid = (hi & ASID_MASK) as u8;
441 for (i, e) in self.entries.iter().enumerate() {
442 // The FULL EntryHi, not a VPN2-masked copy: masking would clear the
443 // R field and make every non-zero region fail to probe.
444 if Self::matches(e, hi, asid) {
445 cop0.set_hardware(reg::INDEX, i as u64);
446 return;
447 }
448 }
449 cop0.set_hardware(reg::INDEX, 1 << 31);
450 }
451
452 /// Does any entry match, without translating or shutting down?
453 ///
454 /// Used by the instruction-fetch path to decide whether a micro-TLB reload
455 /// can actually happen: the 3-PCycle penalty is *"incurred when the
456 /// micro-TLB is updated from the JTLB"* (UM §4.6.2), so a lookup that misses
457 /// the JTLB too must not be charged for a reload that never occurred.
458 #[must_use]
459 pub fn jtlb_has_match(&self, vaddr: u64, asid: u8) -> bool {
460 !self.shutdown && self.entries.iter().any(|e| Self::matches(e, vaddr, asid))
461 }
462
463 /// Probe the instruction micro-TLB, reporting whether it hit.
464 ///
465 /// A miss costs [`ITLB_MISS_PCYCLES`] and is a **stall**, not an exception —
466 /// the JTLB is then consulted, and only a JTLB miss raises.
467 pub fn itlb_probe(&mut self, vaddr: u64, asid: u8) -> bool {
468 for slot in 0..ITLB_ENTRIES {
469 if let Some(i) = self.itlb[slot]
470 && Self::matches(&self.entries[i], vaddr, asid)
471 {
472 self.itlb_lru = 1 - slot;
473 return true;
474 }
475 }
476 false
477 }
478
479 /// Fill a micro-TLB way from the JTLB after a miss.
480 pub fn itlb_fill(&mut self, vaddr: u64, asid: u8) {
481 for (i, e) in self.entries.iter().enumerate() {
482 if Self::matches(e, vaddr, asid) {
483 let way = self.itlb_lru;
484 self.itlb[way] = Some(i);
485 self.itlb_lru = 1 - way;
486 return;
487 }
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 /// Build a COP0 primed to write one entry, then write it.
497 fn install(tlb: &mut Tlb, index: usize, vpn2_addr: u64, asid: u8, lo0: u64, lo1: u64) {
498 let mut c = Cop0::new();
499 c.set_hardware(reg::PAGE_MASK, 0);
500 c.set_hardware(reg::ENTRY_HI, vpn2_addr | u64::from(asid));
501 c.set_hardware(reg::ENTRY_LO0, lo0);
502 c.set_hardware(reg::ENTRY_LO1, lo1);
503 tlb.write_entry(index, &c);
504 }
505
506 /// `V | D | C=3`, i.e. a valid writable cached page at `pfn`.
507 const fn lo(pfn: u64) -> u64 {
508 (pfn << 6) | (3 << 3) | 0b110
509 }
510
511 #[test]
512 fn a_matching_valid_entry_translates() {
513 let mut t = Tlb::new();
514 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
515 // Even page of the pair.
516 let r = t.lookup(0x0000_2000, 0, false).expect("hit");
517 assert_eq!(r.addr, 0x100 * 0x1000);
518 // Odd page.
519 let r = t.lookup(0x0000_3000, 0, false).expect("hit");
520 assert_eq!(r.addr, 0x101 * 0x1000);
521 // Offset within the page is preserved.
522 let r = t.lookup(0x0000_2ABC, 0, false).expect("hit");
523 assert_eq!(r.addr, 0x100 * 0x1000 + 0xABC);
524 }
525
526 #[test]
527 fn no_matching_entry_is_a_refill() {
528 let mut t = Tlb::new();
529 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
530 assert_eq!(t.lookup(0x0040_0000, 0, false), Err(TlbFault::Refill));
531 }
532
533 /// **The `V`-bit rule.** An invalid entry still *matches*, so it raises TLB
534 /// Invalid (general vector) rather than falling through to a refill (refill
535 /// vector). Checking `V` while matching sends the handler to the wrong
536 /// place, and passes any test that only checks "it failed".
537 #[test]
538 fn an_invalid_entry_matches_and_raises_invalid_not_refill() {
539 let mut t = Tlb::new();
540 // V clear on the even page.
541 install(
542 &mut t,
543 0,
544 0x0000_2000,
545 0,
546 (0x100 << 6) | (3 << 3) | 0b100,
547 lo(0x101),
548 );
549 assert_eq!(
550 t.lookup(0x0000_2000, 0, false),
551 Err(TlbFault::Invalid),
552 "matched-but-invalid is NOT a refill"
553 );
554 // The odd page of the same pair is still fine.
555 assert!(t.lookup(0x0000_3000, 0, false).is_ok());
556 }
557
558 /// `D` means **writable**. A store to a clean page raises TLB Modified; a
559 /// load from it does not.
560 #[test]
561 fn a_store_to_a_non_dirty_page_raises_modified() {
562 let mut t = Tlb::new();
563 install(
564 &mut t,
565 0,
566 0x0000_2000,
567 0,
568 (0x100 << 6) | (3 << 3) | 0b010,
569 lo(0x101),
570 );
571 assert!(t.lookup(0x0000_2000, 0, false).is_ok(), "loads are fine");
572 assert_eq!(
573 t.lookup(0x0000_2000, 0, true),
574 Err(TlbFault::Modified),
575 "stores are not"
576 );
577 }
578
579 /// `ASID` gates matching unless `G` is set.
580 #[test]
581 fn asid_gates_matching_unless_global() {
582 let mut t = Tlb::new();
583 install(&mut t, 0, 0x0000_2000, 7, lo(0x100), lo(0x101));
584 assert!(t.lookup(0x0000_2000, 7, false).is_ok(), "matching ASID");
585 assert_eq!(
586 t.lookup(0x0000_2000, 8, false),
587 Err(TlbFault::Refill),
588 "different ASID does not match"
589 );
590
591 // Now global: G set in BOTH halves.
592 let mut t = Tlb::new();
593 install(&mut t, 0, 0x0000_2000, 7, lo(0x100) | 1, lo(0x101) | 1);
594 assert!(
595 t.lookup(0x0000_2000, 99, false).is_ok(),
596 "G ignores the ASID"
597 );
598 }
599
600 /// `G` is the **AND** of the two halves (UM Fig. 5-10). An OR would make far
601 /// too many entries global, and global entries match every ASID — so the bug
602 /// shows up as address-space leakage, not as a missing translation.
603 #[test]
604 fn global_is_the_and_of_both_halves_not_the_or() {
605 let mut t = Tlb::new();
606 install(&mut t, 0, 0x0000_2000, 7, lo(0x100) | 1, lo(0x101));
607 assert!(!t.entry(0).global, "one half set is not global");
608 assert_eq!(
609 t.lookup(0x0000_2000, 99, false),
610 Err(TlbFault::Refill),
611 "and so a foreign ASID must not match"
612 );
613 }
614
615 /// Two coinciding entries shut the TLB down (UM §5.1, p. 122), **including
616 /// when one is invalid** (UM Fig. 6-6, p. 167) — which is the same fact as
617 /// the `V`-not-in-matching rule, seen from the other side.
618 #[test]
619 fn duplicate_entries_trigger_tlb_shutdown_even_when_one_is_invalid() {
620 let mut t = Tlb::new();
621 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
622 // Same VPN2, and deliberately INVALID.
623 install(&mut t, 5, 0x0000_2000, 0, (0x200 << 6) | (3 << 3), 0);
624 assert!(!t.is_shutdown(), "not until a lookup notices");
625 let _ = t.lookup(0x0000_2000, 0, false);
626 assert!(
627 t.is_shutdown(),
628 "an invalid duplicate must still cause shutdown"
629 );
630 }
631
632 /// `TLBP` reports the index, and sets `Index.P` on a miss.
633 #[test]
634 fn tlbp_reports_the_index_or_sets_the_probe_failure_bit() {
635 let mut t = Tlb::new();
636 install(&mut t, 9, 0x0000_2000, 0, lo(0x100), lo(0x101));
637
638 let mut c = Cop0::new();
639 c.set_hardware(reg::ENTRY_HI, 0x0000_2000);
640 t.probe(&mut c);
641 assert_eq!(c.read(reg::INDEX), 9);
642
643 c.set_hardware(reg::ENTRY_HI, 0x0080_0000);
644 t.probe(&mut c);
645 assert_ne!(
646 c.read(reg::INDEX) & (1 << 31),
647 0,
648 "Index.P set on a failed probe"
649 );
650 }
651
652 /// `TLBR` round-trips an entry, and puts the entry's `G` back into **both**
653 /// `EntryLo` halves — `EntryHi` has no `G` field to hold it.
654 #[test]
655 fn tlbr_round_trips_and_restores_g_to_both_halves() {
656 let mut t = Tlb::new();
657 install(&mut t, 3, 0x0000_2000, 7, lo(0x100) | 1, lo(0x101) | 1);
658
659 let mut c = Cop0::new();
660 t.read_entry(3, &mut c);
661 assert_eq!(c.read(reg::ENTRY_HI) & 0xFF, 7, "ASID");
662 assert_eq!(c.read(reg::ENTRY_HI) & 0xFFFF_E000, 0x0000_2000, "VPN2");
663 assert_eq!(c.read(reg::ENTRY_LO0) & 1, 1, "G restored to lo0");
664 assert_eq!(c.read(reg::ENTRY_LO1) & 1, 1, "and to lo1");
665 assert_eq!((c.read(reg::ENTRY_LO0) >> 6) & 0xF_FFFF, 0x100, "PFN");
666 }
667
668 /// Larger page sizes map larger regions, and the even/odd split moves with
669 /// the size rather than staying at 4K.
670 #[test]
671 fn page_mask_selects_the_page_size_and_moves_the_even_odd_split() {
672 let mut t = Tlb::new();
673 let mut c = Cop0::new();
674 // 16K pages: PageMask bits 24:13 = 0b000000000011.
675 c.set_hardware(reg::PAGE_MASK, 0b11 << 13);
676 c.set_hardware(reg::ENTRY_HI, 0x0001_0000);
677 c.set_hardware(reg::ENTRY_LO0, lo(0x100));
678 c.set_hardware(reg::ENTRY_LO1, lo(0x200));
679 t.write_entry(0, &c);
680
681 assert_eq!(Tlb::page_size(0b11 << 13), 0x4000, "16 KiB page");
682 assert_eq!(Tlb::pair_size(0b11 << 13), 0x8000, "32 KiB pair");
683 // PFN is in 4 KiB units regardless of page size, and its low bits are
684 // masked off rather than scaled.
685 let r = t.lookup(0x0001_0000, 0, false).expect("even page");
686 assert_eq!(r.addr, 0x100 << 12);
687 // The split is at 16K now, not 4K.
688 let r = t.lookup(0x0001_4000, 0, false).expect("odd page");
689 assert_eq!(r.addr, (0x200 << 12) & !0x3FFF);
690 }
691
692 /// Only `C == 2` is uncached; the VR4400's other coherency encodings all
693 /// collapse to "cached" on a part with no coherency protocol.
694 #[test]
695 fn only_cache_attribute_two_is_uncached() {
696 for c in 0..8u8 {
697 let p = PageEntry {
698 pfn: 0,
699 c,
700 dirty: true,
701 valid: true,
702 };
703 assert_eq!(p.uncached(), c == 2, "C = {c}");
704 }
705 }
706
707 /// A micro-TLB miss is a **stall**, not an exception: it consults the JTLB
708 /// and fills. Only a JTLB miss raises.
709 #[test]
710 fn the_micro_itlb_misses_then_fills_without_raising() {
711 let mut t = Tlb::new();
712 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
713 assert!(!t.itlb_probe(0x0000_2000, 0), "cold: a miss");
714 t.itlb_fill(0x0000_2000, 0);
715 assert!(t.itlb_probe(0x0000_2000, 0), "warm: a hit");
716 assert_eq!(ITLB_MISS_PCYCLES, 3, "UM §4.6.2 p.107");
717 }
718
719 /// Writing an entry must invalidate the micro-TLB, which caches indices.
720 #[test]
721 fn writing_an_entry_invalidates_the_micro_itlb() {
722 let mut t = Tlb::new();
723 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
724 t.itlb_fill(0x0000_2000, 0);
725 assert!(t.itlb_probe(0x0000_2000, 0));
726 install(&mut t, 1, 0x0000_8000, 0, lo(0x300), lo(0x301));
727 assert!(
728 !t.itlb_probe(0x0000_2000, 0),
729 "a TLB write must not leave the ITLB serving a stale mapping"
730 );
731 }
732
733 /// **All-zero is not a usable reset state**, and the reason is subtle: with
734 /// every entry at `VPN2 = 0` and `V` not participating in matching, the
735 /// first access to page-pair 0 matches all 32 entries and shuts the TLB
736 /// down. Entries must therefore start distinct.
737 /// `EntryHi`'s tag keeps every bit `PageMask` does **not** cover, and a
738 /// non-contiguous mask is what separates masking from dividing.
739 ///
740 /// `VA / pair_size * pair_size` clears the *low* bits of the tag, which is
741 /// right for every legal page size — they are all contiguous runs from bit
742 /// 13. A canonicalized mask need not be: `0b11_11_11_11_00` covers bits
743 /// 22:15 and leaves 14:13 alone, and division cannot express that.
744 #[test]
745 fn entry_hi_read_back_clears_exactly_the_page_mask_bits() {
746 let mut t = Tlb::new();
747 let mut c = Cop0::new();
748 for (mask, want) in [
749 (0b00_00_00_00_00u32, 0xFFFF_E0FFu64),
750 (0b00_00_00_00_11, 0xFFFF_80FF),
751 (0b00_00_00_11_11, 0xFFFE_00FF),
752 (0b11_11_11_11_11, 0xFF80_00FF),
753 // The one with a hole: division gets this wrong and masking does not.
754 (0b11_11_11_11_00, 0xFF80_60FF),
755 ] {
756 c.set_hardware(reg::PAGE_MASK, u64::from(mask << 13));
757 c.set_hardware(reg::ENTRY_HI, 0xFFFF_E0FF);
758 c.set_hardware(reg::ENTRY_LO0, 0x3FFF_FFFF);
759 c.set_hardware(reg::ENTRY_LO1, 0x3FFF_FFFF);
760 t.write_entry(0, &c);
761 c.set_hardware(reg::ENTRY_HI, 0xFFFF_FFFF_FFFF_FFFF);
762 t.read_entry(0, &mut c);
763 assert_eq!(c.read(reg::ENTRY_HI), want, "mask {mask:#014b}");
764 }
765 }
766
767 /// `PageMask` bits 24:13 are six 2-bit pairs, and a pair stores its HIGHER
768 /// bit only: `0b10` becomes `0b11`, `0b01` becomes `0b00`.
769 ///
770 /// The natural implementation — keep the value, mask to 24:13 — is wrong in
771 /// both directions and silently so. It accepts page sizes the hardware has
772 /// no encoding for, and it reports back a mask that was never stored.
773 #[test]
774 fn a_page_mask_pair_stores_only_its_higher_bit() {
775 let m = |v: u32| Tlb::canonical_page_mask(v << 13);
776 // The six legal sizes round-trip unchanged.
777 // Grouped in PAIRS deliberately: that is the unit the hardware stores,
778 // so a mask that looks wrong here is wrong.
779 for legal in [
780 0b0,
781 0b11,
782 0b11_11,
783 0b11_11_11,
784 0b11_11_11_11,
785 0b11_11_11_11_11,
786 ] {
787 assert_eq!(m(legal), legal << 13, "{legal:#b} is a legal mask");
788 }
789 // A lone LOW bit is discarded; a lone HIGH bit fills its pair.
790 assert_eq!(m(0b00_00_00_00_01), 0, "low bit of pair 0 discarded");
791 assert_eq!(
792 m(0b00_00_00_00_10),
793 0b11 << 13,
794 "high bit of pair 0 fills it"
795 );
796 assert_eq!(m(0b00_00_00_01_00), 0, "low bit of pair 1 discarded");
797 assert_eq!(
798 m(0b00_00_00_10_00),
799 0b11_00 << 13,
800 "high bit of pair 1 fills it"
801 );
802 // Mixed: each pair decided independently, from its own higher bit.
803 assert_eq!(m(0b00_11_00_01_10_10_01), 0b11_00_00_11_11_00 << 13);
804 }
805
806 #[test]
807 fn a_fresh_tlb_does_not_shut_down_on_the_first_low_access() {
808 let mut t = Tlb::new();
809 assert_eq!(t.lookup(0x0000_0000, 0, false), Err(TlbFault::Refill));
810 assert!(
811 !t.is_shutdown(),
812 "a power-on TLB must not self-destruct on its first lookup"
813 );
814 // And the tags really are distinct, which is what guarantees it.
815 for i in 0..JTLB_ENTRIES {
816 for j in (i + 1)..JTLB_ENTRIES {
817 assert_ne!(
818 t.entry(i).vpn2,
819 t.entry(j).vpn2,
820 "entries {i} and {j} coincide at reset"
821 );
822 }
823 }
824 }
825
826 /// **The sign-extension bug.** A 32-bit kernel address arrives
827 /// sign-extended — KSEG3 is `0xFFFF_FFFF_E000_0000`, not `0xE000_0000` —
828 /// while `EntryHi.VPN2` holds only VA(39:13).
829 ///
830 /// Comparing the raw 64-bit value pits the sign extension against zero, so
831 /// **no mapped kernel address ever matches**, however correct the entry.
832 /// Every test above uses KUSEG, which has no sign extension — which is
833 /// exactly why the suite was silent about it until review pointed it out.
834 #[test]
835 fn a_sign_extended_kernel_address_matches_its_entry() {
836 let mut t = Tlb::new();
837 let mut c = Cop0::new();
838 // Software in 32-bit mode writes EntryHi through MTC0, which
839 // sign-extends -- so the entry carries R = 3 as well.
840 c.set_hardware(reg::PAGE_MASK, 0);
841 c.set_hardware(reg::ENTRY_HI, 0xFFFF_FFFF_E000_0000);
842 c.set_hardware(reg::ENTRY_LO0, lo(0x100) | 1);
843 c.set_hardware(reg::ENTRY_LO1, lo(0x101) | 1);
844 t.write_entry(0, &c);
845
846 let r = t
847 .lookup(0xFFFF_FFFF_E000_0000, 0, false)
848 .expect("KSEG3 must translate");
849 assert_eq!(r.addr, 0x100 << 12);
850
851 // And the probe path must agree -- it previously masked the region away.
852 let mut c2 = Cop0::new();
853 c2.set_hardware(reg::ENTRY_HI, 0xFFFF_FFFF_E000_0000);
854 t.probe(&mut c2);
855 assert_eq!(c2.read(reg::INDEX), 0, "TLBP must find it too");
856 }
857
858 /// The `R` region field participates in matching: the same low address in a
859 /// different region is a different translation.
860 #[test]
861 fn the_region_field_distinguishes_otherwise_identical_addresses() {
862 let mut t = Tlb::new();
863 let mut c = Cop0::new();
864 c.set_hardware(reg::PAGE_MASK, 0);
865 // Region 0 (xkuseg).
866 c.set_hardware(reg::ENTRY_HI, 0x0000_0000_0000_2000);
867 c.set_hardware(reg::ENTRY_LO0, lo(0x100) | 1);
868 c.set_hardware(reg::ENTRY_LO1, lo(0x101) | 1);
869 t.write_entry(0, &c);
870
871 assert!(t.lookup(0x0000_2000, 0, false).is_ok());
872 assert_eq!(
873 t.lookup(0xC000_0000_0000_2000, 0, false),
874 Err(TlbFault::Refill),
875 "same VPN2, different region -- must not match"
876 );
877 }
878
879 /// A shut-down TLB is **unusable** until reset (UM §5.1, Fig. 6-6). Recording
880 /// the flag and then continuing to translate is worse than not tracking it.
881 #[test]
882 fn a_shut_down_tlb_refuses_every_subsequent_lookup() {
883 let mut t = Tlb::new();
884 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
885 install(&mut t, 5, 0x0000_2000, 0, lo(0x200), lo(0x201));
886 let _ = t.lookup(0x0000_2000, 0, false);
887 assert!(t.is_shutdown());
888
889 // A completely unrelated, singly-mapped address must now fail too.
890 install(&mut t, 9, 0x0004_0000, 0, lo(0x300), lo(0x301));
891 assert_eq!(
892 t.lookup(0x0004_0000, 0, false),
893 Err(TlbFault::Refill),
894 "the TLB is dead until reset"
895 );
896 }
897
898 /// `jtlb_has_match` reports matching without translating or shutting down —
899 /// the instruction-fetch path needs to know whether a micro-TLB reload can
900 /// happen *before* deciding to charge for one.
901 #[test]
902 fn jtlb_has_match_is_side_effect_free() {
903 let mut t = Tlb::new();
904 install(&mut t, 0, 0x0000_2000, 0, lo(0x100), lo(0x101));
905 install(&mut t, 5, 0x0000_2000, 0, lo(0x200), lo(0x201));
906 assert!(t.jtlb_has_match(0x0000_2000, 0));
907 assert!(
908 !t.is_shutdown(),
909 "a query must not trip shutdown -- only a real lookup does"
910 );
911 assert!(!t.jtlb_has_match(0x0040_0000, 0));
912 }
913}