Skip to main content

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}