Skip to main content

rustyn64_cpu/
cache.rs

1//! The VR4300 primary caches (T-11-003).
2//!
3//! Two direct-mapped, physically-tagged caches sit between the CPU and the bus:
4//! a **16 KiB instruction cache** with 32-byte lines and an **8 KiB write-back
5//! data cache** with 16-byte lines (UM §11.2, Tables 11-1/11-2).
6//!
7//! # Why they are modeled at all
8//!
9//! They were a deliberate no-op until now (accuracy ledger D-5): with no cache
10//! contents, invalidate and write-back had nothing to act on, which is
11//! observationally sound *only* while nothing can observe staleness. That stops
12//! being true the moment a program writes a location through one path and reads
13//! it through another — which is precisely what n64-systemtest's `DCACHE:` and
14//! `ICACHE:` groups do, and they are the reason this exists.
15//!
16//! # Indexing is PHYSICAL here, virtual on hardware
17//!
18//! The real caches are virtually indexed and physically tagged, so two virtual
19//! addresses mapping to one physical address can occupy two lines (a cache
20//! alias). Indexing by physical address instead makes aliases impossible.
21//!
22//! This is a **deviation, not a simplification that is strictly safer**. Software
23//! that observes aliasing — or that relies on an `Index_*` operation selecting a
24//! line by *virtual* index on a TLB-mapped page — sees different behavior here,
25//! because translation preserves only the low 12 bits while the D-cache index
26//! reaches bit 12 and the I-cache bit 13. What is bounded is the tested scope:
27//! every test that motivated this module operates through KSEG0, where the two
28//! indexings coincide. Accuracy ledger **D-6**.
29//!
30//! # `TagLo`
31//!
32//! `Index_Load_Tag` and `Index_Store_Tag` move a line's tag through COP0
33//! `TagLo`, whose layout differs per cache (UM §5.3, Figures 5-19/5-20):
34//!
35//! ```text
36//!   bits 27..=8   PTagLo — the physical frame number, PA(31:12)
37//!   bits  7..=6   PState — I-cache: 2 = Valid, 0 = Invalid
38//!                          D-cache: 3 = Valid, 0 = Invalid
39//! ```
40//!
41//! The D-cache's write-back ("dirty") bit is **not** in `TagLo`, so a clean and a
42//! dirty valid line are indistinguishable to `Index_Load_Tag`. That is hardware
43//! behavior, not an omission — see [`Dcache::load_tag`].
44
45use serde::{Deserialize, Serialize};
46
47/// Instruction-cache line size, in bytes (UM §11.2).
48pub const ICACHE_LINE: u32 = 32;
49/// Data-cache line size, in bytes (UM §11.2).
50pub const DCACHE_LINE: u32 = 16;
51/// Lines in the 16 KiB instruction cache.
52pub const ICACHE_LINES: usize = 512;
53/// Lines in the 8 KiB data cache.
54pub const DCACHE_LINES: usize = 512;
55
56/// `PState` for a valid I-cache line.
57const ICACHE_VALID_STATE: u32 = 2;
58/// `PState` for a valid D-cache line.
59const DCACHE_VALID_STATE: u32 = 3;
60
61/// One cache line's tag and data.
62///
63/// `tag` holds the physical frame number, PA(31:12), and survives invalidation:
64/// `Index_Invalidate` clears `valid` and leaves `tag` alone, which is directly
65/// observable through `Index_Load_Tag` and is asserted by n64-systemtest.
66#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
67struct Line<const N: usize> {
68    /// Physical frame number, PA(31:12).
69    tag: u32,
70    /// Whether the line holds valid data.
71    valid: bool,
72    /// Whether the line has been written since it was filled (D-cache only).
73    dirty: bool,
74    /// The line's bytes, in memory order.
75    #[serde(with = "serde_big_array::BigArray")]
76    data: [u8; N],
77}
78
79impl<const N: usize> Line<N> {
80    const EMPTY: Self = Self {
81        tag: 0,
82        valid: false,
83        dirty: false,
84        data: [0; N],
85    };
86}
87
88/// Pack a tag into the `TagLo` layout.
89const fn pack_tag(tag: u32, valid: bool, valid_state: u32) -> u32 {
90    let pstate = if valid { valid_state } else { 0 };
91    (pstate << 6) | ((tag & 0x000F_FFFF) << 8)
92}
93
94/// Unpack a `TagLo` value into `(tag, valid)`.
95///
96/// `valid` requires the cache's **own** `PState` encoding — 2 for the I-cache,
97/// 3 for the D-cache — not merely a non-zero field. The two reserved values (1,
98/// and 2-vs-3 crossed between the caches) are not "valid": treating any non-zero
99/// `PState` as valid would let `Index_Store_Tag` conjure a live line out of an
100/// encoding the hardware does not define.
101const fn unpack_tag(tag_lo: u32, valid_state: u32) -> (u32, bool) {
102    (
103        (tag_lo >> 8) & 0x000F_FFFF,
104        (tag_lo >> 6) & 3 == valid_state,
105    )
106}
107
108/// What a cache operation needs the caller to do on its behalf.
109///
110/// The caches deliberately do **not** hold a bus handle: `rustyn64-cpu` sees the
111/// bus only as a `&mut B` borrowed for the duration of a step, and threading it
112/// into the cache would widen that borrow across the whole pipeline. Instead a
113/// fill or write-back is described here and performed by the caller, which
114/// already has the bus.
115#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
116pub struct Writeback<const N: usize> {
117    /// Physical address of the line's first byte.
118    pub addr: u32,
119    /// The line's bytes.
120    #[serde(with = "serde_big_array::BigArray")]
121    pub data: [u8; N],
122}
123
124/// The 8 KiB write-back data cache.
125#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
126pub struct Dcache {
127    #[serde(with = "serde_big_array::BigArray")]
128    lines: [Line<16>; DCACHE_LINES],
129}
130
131impl Default for Dcache {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl Dcache {
138    /// A cache with every line invalid.
139    #[must_use]
140    pub const fn new() -> Self {
141        Self {
142            lines: [Line::EMPTY; DCACHE_LINES],
143        }
144    }
145
146    /// Which line a physical address maps to.
147    const fn index(addr: u32) -> usize {
148        ((addr / DCACHE_LINE) as usize) % DCACHE_LINES
149    }
150
151    /// The physical address of the line currently resident at `index`.
152    ///
153    /// Reconstructed from the stored tag plus the index bits the tag does not
154    /// carry — a write-back must go to the address the line came *from*, not to
155    /// whatever address provoked the eviction.
156    const fn resident_addr(&self, index: usize) -> u32 {
157        let within_page = (index as u32 * DCACHE_LINE) & 0xFFF;
158        (self.lines[index].tag << 12) | within_page
159    }
160
161    /// Does `addr` hit a valid line?
162    #[must_use]
163    pub const fn hits(&self, addr: u32) -> bool {
164        let i = Self::index(addr);
165        self.lines[i].valid && self.lines[i].tag == addr >> 12
166    }
167
168    /// Prepare the line covering `addr` for access, reporting what the caller
169    /// must do to the bus first.
170    ///
171    /// Returns the eviction that must be written back (if any). After the
172    /// caller has performed it and supplied the fill data through
173    /// [`Dcache::install`], the line is resident.
174    #[must_use]
175    pub const fn miss_plan(&self, addr: u32) -> Option<Option<Writeback<16>>> {
176        let i = Self::index(addr);
177        if self.lines[i].valid && self.lines[i].tag == addr >> 12 {
178            return None;
179        }
180        if self.lines[i].valid && self.lines[i].dirty {
181            Some(Some(Writeback {
182                addr: self.resident_addr(i),
183                data: self.lines[i].data,
184            }))
185        } else {
186            Some(None)
187        }
188    }
189
190    /// Install a freshly filled line.
191    pub const fn install(&mut self, addr: u32, data: [u8; 16]) {
192        let i = Self::index(addr);
193        self.lines[i] = Line {
194            tag: addr >> 12,
195            valid: true,
196            dirty: false,
197            data,
198        };
199    }
200
201    /// Read `len` bytes at `addr` from a resident line.
202    ///
203    /// The caller must have made the line resident first. An access never
204    /// straddles two lines: every access reaching the cache is naturally
205    /// aligned to its own width, and the widest is 8 bytes into a 16-byte line.
206    #[must_use]
207    pub fn read(&self, addr: u32, len: usize) -> u64 {
208        let i = Self::index(addr);
209        let o = (addr % DCACHE_LINE) as usize;
210        let mut v = 0u64;
211        for k in 0..len {
212            v = (v << 8) | u64::from(self.lines[i].data[o + k]);
213        }
214        v
215    }
216
217    /// Write the low `len` bytes of `value` at `addr` into a resident line,
218    /// marking it dirty.
219    pub fn write(&mut self, addr: u32, len: usize, value: u64) {
220        let i = Self::index(addr);
221        let o = (addr % DCACHE_LINE) as usize;
222        for k in 0..len {
223            let shift = 8 * (len - 1 - k);
224            self.lines[i].data[o + k] = (value >> shift) as u8;
225        }
226        self.lines[i].dirty = true;
227    }
228
229    /// `Index_Load_Tag`: the tag at the index `addr` selects, in `TagLo` form.
230    ///
231    /// A dirty line and a clean one both report `PState = 3`. The write-back bit
232    /// has no `TagLo` field on this part, so the distinction is genuinely not
233    /// visible to software — reporting it would be inventing an encoding.
234    #[must_use]
235    pub const fn load_tag(&self, addr: u32) -> u32 {
236        let i = Self::index(addr);
237        pack_tag(self.lines[i].tag, self.lines[i].valid, DCACHE_VALID_STATE)
238    }
239
240    /// `Index_Store_Tag`: overwrite the tag at the index `addr` selects.
241    pub const fn store_tag(&mut self, addr: u32, tag_lo: u32) {
242        let i = Self::index(addr);
243        let (tag, valid) = unpack_tag(tag_lo, DCACHE_VALID_STATE);
244        self.lines[i].tag = tag;
245        self.lines[i].valid = valid;
246        self.lines[i].dirty = false;
247    }
248
249    /// Clear the valid bit at `index`, keeping the tag.
250    const fn invalidate_index(&mut self, i: usize) {
251        self.lines[i].valid = false;
252        self.lines[i].dirty = false;
253    }
254
255    /// Take the line at the index `addr` selects for write-back, if it is dirty.
256    ///
257    /// `clean` clears the dirty bit (`Hit_Write_Back` leaves the line resident);
258    /// `invalidate` additionally clears the valid bit.
259    pub const fn flush_index(
260        &mut self,
261        addr: u32,
262        invalidate: bool,
263        clean: bool,
264    ) -> Option<Writeback<16>> {
265        let i = Self::index(addr);
266        let out = if self.lines[i].valid && self.lines[i].dirty {
267            Some(Writeback {
268                addr: self.resident_addr(i),
269                data: self.lines[i].data,
270            })
271        } else {
272            None
273        };
274        if clean {
275            self.lines[i].dirty = false;
276        }
277        if invalidate {
278            self.invalidate_index(i);
279        }
280        out
281    }
282
283    /// `Create_Dirty_Exclusive`: claim the line for `addr` without filling it.
284    ///
285    /// Returns any dirty line evicted in the process. The new line's data is
286    /// whatever the old line held — the operation exists precisely so software
287    /// can avoid the fill when it is about to overwrite the whole line, so
288    /// leaving stale bytes is the point, not an oversight.
289    pub const fn create_dirty_exclusive(&mut self, addr: u32) -> Option<Writeback<16>> {
290        let i = Self::index(addr);
291        let out = if self.lines[i].valid && self.lines[i].dirty && self.lines[i].tag != addr >> 12 {
292            Some(Writeback {
293                addr: self.resident_addr(i),
294                data: self.lines[i].data,
295            })
296        } else {
297            None
298        };
299        self.lines[i].tag = addr >> 12;
300        self.lines[i].valid = true;
301        self.lines[i].dirty = true;
302        out
303    }
304}
305
306/// The 16 KiB instruction cache.
307#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
308pub struct Icache {
309    #[serde(with = "serde_big_array::BigArray")]
310    lines: [Line<32>; ICACHE_LINES],
311}
312
313impl Default for Icache {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319impl Icache {
320    /// A cache with every line invalid.
321    #[must_use]
322    #[expect(
323        clippy::large_stack_arrays,
324        reason = "a cache is a fixed hardware structure that lives inside System \
325                  for the whole run, not a stack temporary; boxing it would cost \
326                  Pipeline::new its const-ness for no benefit"
327    )]
328    pub const fn new() -> Self {
329        Self {
330            lines: [Line::EMPTY; ICACHE_LINES],
331        }
332    }
333
334    /// Which line a physical address maps to.
335    const fn index(addr: u32) -> usize {
336        ((addr / ICACHE_LINE) as usize) % ICACHE_LINES
337    }
338
339    /// The physical address of the line currently resident at `index`.
340    const fn resident_addr(&self, index: usize) -> u32 {
341        let within_page = (index as u32 * ICACHE_LINE) & 0xFFF;
342        (self.lines[index].tag << 12) | within_page
343    }
344
345    /// Does `addr` hit a valid line?
346    #[must_use]
347    pub const fn hits(&self, addr: u32) -> bool {
348        let i = Self::index(addr);
349        self.lines[i].valid && self.lines[i].tag == addr >> 12
350    }
351
352    /// The instruction word at `addr` from a resident line.
353    #[must_use]
354    pub const fn read_word(&self, addr: u32) -> u32 {
355        let i = Self::index(addr);
356        let o = (addr % ICACHE_LINE) as usize;
357        u32::from_be_bytes([
358            self.lines[i].data[o],
359            self.lines[i].data[o + 1],
360            self.lines[i].data[o + 2],
361            self.lines[i].data[o + 3],
362        ])
363    }
364
365    /// Install a freshly filled line.
366    ///
367    /// The I-cache is read-only to the CPU, so a fill never evicts anything that
368    /// needs writing back — only `Hit_Write_Back` can move data outward, and it
369    /// is explicitly requested.
370    pub const fn install(&mut self, addr: u32, data: [u8; 32]) {
371        let i = Self::index(addr);
372        self.lines[i] = Line {
373            tag: addr >> 12,
374            valid: true,
375            dirty: false,
376            data,
377        };
378    }
379
380    /// `Index_Load_Tag`, in `TagLo` form.
381    #[must_use]
382    pub const fn load_tag(&self, addr: u32) -> u32 {
383        let i = Self::index(addr);
384        pack_tag(self.lines[i].tag, self.lines[i].valid, ICACHE_VALID_STATE)
385    }
386
387    /// `Index_Store_Tag`: overwrite the tag at the index `addr` selects.
388    pub const fn store_tag(&mut self, addr: u32, tag_lo: u32) {
389        let i = Self::index(addr);
390        let (tag, valid) = unpack_tag(tag_lo, ICACHE_VALID_STATE);
391        self.lines[i].tag = tag;
392        self.lines[i].valid = valid;
393    }
394
395    /// Clear the valid bit at the index `addr` selects, keeping the tag.
396    pub const fn invalidate_index(&mut self, addr: u32) {
397        let i = Self::index(addr);
398        self.lines[i].valid = false;
399    }
400
401    /// Invalidate the line covering `addr`, but only if it is resident.
402    pub const fn hit_invalidate(&mut self, addr: u32) {
403        if self.hits(addr) {
404            self.invalidate_index(addr);
405        }
406    }
407
408    /// `Hit_Write_Back`: push a resident line's contents out to memory.
409    ///
410    /// The I-cache has no dirty bit and the CPU never writes it, so on hardware
411    /// this can only ever rewrite memory with what it already held — unless
412    /// memory was changed underneath it, which is exactly the case
413    /// n64-systemtest constructs.
414    #[must_use]
415    pub const fn flush_hit(&self, addr: u32) -> Option<Writeback<32>> {
416        if !self.hits(addr) {
417            return None;
418        }
419        let i = Self::index(addr);
420        Some(Writeback {
421            addr: self.resident_addr(i),
422            data: self.lines[i].data,
423        })
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn a_fill_then_read_returns_the_filled_bytes() {
433        let mut d = Dcache::new();
434        let mut data = [0u8; 16];
435        data[4] = 0xDE;
436        data[5] = 0xAD;
437        data[6] = 0xBE;
438        data[7] = 0xEF;
439        d.install(0x1000, data);
440        assert_eq!(d.read(0x1004, 4), 0xDEAD_BEEF);
441    }
442
443    #[test]
444    fn invalidating_keeps_the_tag_so_load_tag_still_reports_the_pfn() {
445        // The distinction the ROM asserts: `Index_Invalidate` clears PState and
446        // leaves PTagLo alone. Clearing both would report tag 0 and pass any
447        // test that only looked at the valid bit.
448        let mut d = Dcache::new();
449        d.install(0x2_5000, [0; 16]);
450        assert_eq!(d.load_tag(0x2_5000), (3 << 6) | (0x25 << 8));
451        d.flush_index(0x2_5000, true, false);
452        assert_eq!(d.load_tag(0x2_5000), 0x25 << 8, "PFN survives invalidation");
453    }
454
455    #[test]
456    fn a_dirty_line_is_written_back_to_the_address_it_came_from() {
457        // Not to the address that evicted it. The two differ in exactly the bits
458        // the index does not carry, so a cache small enough to alias is the only
459        // place the mistake shows up -- which is every real eviction.
460        let mut d = Dcache::new();
461        d.install(0x1_0000, [0xAA; 16]);
462        d.write(0x1_0000, 4, 0x1122_3344);
463        let plan = d.miss_plan(0x2_0000).expect("different tag misses");
464        assert_eq!(plan.expect("dirty line evicts").addr, 0x1_0000);
465    }
466
467    #[test]
468    fn a_clean_eviction_needs_no_writeback() {
469        let mut d = Dcache::new();
470        d.install(0x1_0000, [0xAA; 16]);
471        assert_eq!(d.miss_plan(0x2_0000), Some(None));
472    }
473
474    #[test]
475    fn a_hit_needs_no_plan_at_all() {
476        let mut d = Dcache::new();
477        d.install(0x1_0000, [0xAA; 16]);
478        assert_eq!(d.miss_plan(0x1_0008), None, "same line");
479    }
480
481    /// `Index_Store_Tag` requires the cache's OWN valid encoding. Accepting any
482    /// non-zero `PState` conjures a live line from an undefined one — and note
483    /// the two caches disagree, so the D-cache's 3 must not validate an I-cache
484    /// line either.
485    #[test]
486    fn store_tag_rejects_a_pstate_this_cache_does_not_define() {
487        let mut d = Dcache::new();
488        let mut i = Icache::new();
489        for bad in [1u32, 2] {
490            d.store_tag(0x1000, (bad << 6) | (0x1 << 8));
491            assert!(!d.hits(0x1000), "D-cache PState {bad} is not Valid");
492        }
493        for bad in [1u32, 3] {
494            i.store_tag(0x1000, (bad << 6) | (0x1 << 8));
495            assert!(!i.hits(0x1000), "I-cache PState {bad} is not Valid");
496        }
497        d.store_tag(0x1000, (3 << 6) | (0x1 << 8));
498        assert!(d.hits(0x1000), "3 is the D-cache's Valid");
499        i.store_tag(0x1000, (2 << 6) | (0x1 << 8));
500        assert!(i.hits(0x1000), "2 is the I-cache's Valid");
501    }
502
503    #[test]
504    fn store_tag_round_trips_through_load_tag() {
505        let mut d = Dcache::new();
506        let want = (3 << 6) | (0x1AC << 8);
507        d.store_tag(0x1AC_000, want);
508        assert_eq!(d.load_tag(0x1AC_000), want);
509    }
510
511    #[test]
512    fn the_two_caches_use_different_valid_states() {
513        // I-cache Valid is 2, D-cache Valid is 3 (UM Figures 5-19/5-20). Sharing
514        // one constant would pass every test that only checked "non-zero".
515        let mut i = Icache::new();
516        let mut d = Dcache::new();
517        i.install(0x1000, [0; 32]);
518        d.install(0x1000, [0; 16]);
519        assert_eq!((i.load_tag(0x1000) >> 6) & 3, 2);
520        assert_eq!((d.load_tag(0x1000) >> 6) & 3, 3);
521    }
522
523    #[test]
524    fn hit_writeback_only_reports_a_resident_line() {
525        let mut i = Icache::new();
526        assert_eq!(i.flush_hit(0x1000), None);
527        i.install(0x1000, [0x5A; 32]);
528        assert_eq!(i.flush_hit(0x1000).expect("resident").addr, 0x1000);
529        assert_eq!(i.flush_hit(0x2000), None, "different tag, same index");
530    }
531
532    #[test]
533    fn create_dirty_exclusive_claims_the_line_without_a_fill() {
534        let mut d = Dcache::new();
535        assert_eq!(d.create_dirty_exclusive(0x3000), None);
536        assert!(d.hits(0x3000));
537        assert_eq!((d.load_tag(0x3000) >> 6) & 3, 3);
538    }
539}