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}