cache.rsannotatedcache.rssource435 lines · 14.5 KB · raw

SH7604 CPU cache

This is a 4-way set-associative cache that uses a pseudo-LRU algorithm for cache replacement.

Cache replacement is performed when a cached read misses. The cache is write-through, so writes will only update cache if there is a cache hit.

Games that are known to depend on CPU cache emulation (specifically data cache):

  • WWF Raw (32X) writes to cartridge ROM addresses and expects to be able to read back the written values from CPU cache. Without cache, it won't correctly populate 32X palette RAM which causes missing graphics in menus.
  • Pitfall: The Mayan Adventure (32X) writes to addresses around $00090000 (out-of-bounds in boot ROM area) and expects to be able to read back the written values from CPU cache. Without cache, objects/"sprites" that are partially offscreen will not display at all until they are entirely onscreen.
17use crate::debug::CacheDebugState;
18use bincode::{Decode, Encode};
19use jgenesis_common::boxedarray::BoxedWordArray;
20use jgenesis_common::debug::{DebugMemoryView, DebugWordsView, Endian};
21use jgenesis_common::num::{GetBit, U16Ext};
22use std::array;
24const CACHE_RAM_LEN_WORDS: usize = 4 * 1024 / 2;
25
26const WAYS: usize = 4;

Cache lines are 16 bytes and there are 4 ways in each cache line 4096 / 16 / 4 = 64

30const CACHE_ENTRIES: usize = 64;
32#[derive(Debug, Clone, Default, Encode, Decode)]
33pub struct CacheControlRegister {
34    // Specifies which way is accessed when the address array is accessed directly
35    pub way: u8,
36    pub mode: CacheMode,
37    pub disable_data_replacement: bool,
38    pub disable_instruction_replacement: bool,
39    pub cache_enabled: bool,
40}
41
42impl CacheControlRegister {
43    fn read(&self) -> u8 {
44        (self.way << 6)
45            | ((self.mode as u8) << 3)
46            | (u8::from(self.disable_data_replacement) << 2)
47            | (u8::from(self.disable_instruction_replacement) << 1)
48            | u8::from(self.cache_enabled)
49    }
50
51    fn write(&mut self, value: u8) {
52        self.way = value >> 6;
53        self.mode = CacheMode::from_bit(value.bit(3));
54        self.disable_data_replacement = value.bit(2);
55        self.disable_instruction_replacement = value.bit(1);
56        self.cache_enabled = value.bit(0);
57
58        log::trace!("CCR write: {value:02X}");
59        log::trace!("  Way specification: {}", self.way);
60        log::trace!("  Cache mode: {:?}", self.mode);
61        log::trace!("  Cache purged: {}", value.bit(4));
62        log::trace!("  Disable data replacement: {}", self.disable_data_replacement);
63        log::trace!("  Disable instruction replacement: {}", self.disable_instruction_replacement);
64        log::trace!("  Cache enabled: {}", self.cache_enabled);
65    }
66}
67
68#[derive(Debug, Clone, Encode, Decode)]
69struct Way {
70    // Tag is address bits 10-28
71    tags: [u32; CACHE_ENTRIES],
72    valid_bits: u64,
73}
74
75impl Way {
76    fn new() -> Self {
77        Self { tags: array::from_fn(|_| 0), valid_bits: 0 }
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
82pub enum CacheMode {
83    #[default]
84    FourWay = 0,
85    TwoWay = 1,
86}
87
88impl CacheMode {
89    fn from_bit(bit: bool) -> Self {
90        if bit { Self::TwoWay } else { Self::FourWay }
91    }
92}
93
94#[derive(Debug, Clone, Encode, Decode)]
95pub struct CpuCache {
96    // Store cache as u16s because the most common fetches are opcodes which are 16-bit
97    ram: BoxedWordArray<CACHE_RAM_LEN_WORDS>,
98    ways: Box<[Way; WAYS]>,
99    lru_bits: [u8; CACHE_ENTRIES],
100    control: CacheControlRegister,
101}
102
103impl CpuCache {
104    pub fn new() -> Self {
105        Self {
106            ram: BoxedWordArray::new(),
107            ways: Box::new(array::from_fn(|_| Way::new())),
108            lru_bits: array::from_fn(|_| 0),
109            control: CacheControlRegister::default(),
110        }
111    }
112
113    pub fn read_u8(&mut self, address: u32) -> Option<u8> {
114        self.cache_read(address, move |cache, way_idx, entry_idx| {
115            let address = cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xF);
116            cache.ram[address >> 1].to_be_bytes()[address & 1]
117        })
118    }
119
120    #[inline(always)]
121    pub fn read_u16(&mut self, address: u32) -> Option<u16> {
122        self.cache_read(address, move |cache, way_idx, entry_idx| {
123            let address = cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xE);
124            cache.ram[address >> 1]
125        })
126    }
127
128    pub fn read_u32(&mut self, address: u32) -> Option<u32> {
129        self.cache_read(address, move |cache, way_idx, entry_idx| {
130            let address = (cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xC)) >> 1;
131            let high_word = cache.ram[address];
132            let low_word = cache.ram[address + 1];
133            (u32::from(high_word) << 16) | u32::from(low_word)
134        })
135    }
136
137    #[inline(always)]
138    fn cache_read<T>(
139        &mut self,
140        address: u32,
141        read_fn: impl FnOnce(&Self, usize, usize) -> T,
142    ) -> Option<T> {
143        if !self.control.cache_enabled {
144            return None;
145        }
146
147        let entry_idx = cache_entry_index(address);
148        let tag = tag_address(address);
149
150        // Iterate in reverse for slightly better performance when cache is in 2-way mode.
151        // Per SH7604 documentation, all 4 ways are checked even in 2-way mode; 2-way mode only
152        // changes replacement behavior
153        for way_idx in (0..4).rev() {
154            if self.ways[way_idx].valid_bits.bit(entry_idx as u8)
155                && self.ways[way_idx].tags[entry_idx] == tag
156            {
157                self.update_lru_bits(way_idx, entry_idx);
158                return Some(read_fn(self, way_idx, entry_idx));
159            }
160        }
161
162        None
163    }
164
165    pub fn peek(&self, address: u32) -> Option<u16> {
166        if !self.control.cache_enabled {
167            return None;
168        }
169
170        let entry_idx = cache_entry_index(address);
171        let tag = tag_address(address);
172
173        for way_idx in (0..4).rev() {
174            if self.ways[way_idx].valid_bits.bit(entry_idx as u8)
175                && self.ways[way_idx].tags[entry_idx] == tag
176            {
177                let address = cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xE);
178                return Some(self.ram[address >> 1]);
179            }
180        }
181
182        None
183    }
184
185    pub fn peek_data_array(&self, address: u32) -> u16 {
186        self.ram[((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1)]
187    }
188
189    #[inline]
190    pub fn should_replace_instruction(&self) -> bool {
191        self.control.cache_enabled && !self.control.disable_instruction_replacement
192    }
193
194    #[inline]
195    pub fn should_replace_data(&self) -> bool {
196        self.control.cache_enabled && !self.control.disable_data_replacement
197    }
198
199    #[must_use]
200    pub fn replace(&mut self, address: u32, cache_line: [u16; 8]) -> u32 {
201        let entry_idx = cache_entry_index(address);
202
203        let lru_bits = self.lru_bits[entry_idx];
204        let way_idx = match self.control.mode {
205            CacheMode::FourWay => {
206                usize::from(lru_bits & 0b100110 == 0b000110)
207                    | (usize::from(lru_bits & 0b010101 == 0b000001) << 1)
208                    | (3 * usize::from(lru_bits & 0b001011 == 0))
209            }
210            CacheMode::TwoWay => {
211                if lru_bits.bit(0) {
212                    2
213                } else {
214                    3
215                }
216            }
217        };
218
219        self.ways[way_idx].tags[entry_idx] = tag_address(address);
220        self.ways[way_idx].valid_bits |= 1 << entry_idx;
221        self.update_lru_bits(way_idx, entry_idx);
222
223        let ram_addr = cache_ram_addr(way_idx, entry_idx) >> 1;
224        self.ram[ram_addr..ram_addr + 8].copy_from_slice(&cache_line);
225
226        let cache_line_addr = ((address >> 1) & 7 & !1) as usize;
227        let high: u32 = cache_line[cache_line_addr].into();
228        let low: u32 = cache_line[cache_line_addr + 1].into();
229        low | (high << 16)
230    }
231
232    #[inline(always)]
233    fn update_lru_bits(&mut self, way_idx: usize, entry_idx: usize) {
234        // Bit 5: 0 -> 1
235        // Bit 4: 0 -> 2
236        // Bit 3: 0 -> 3
237        // Bit 2: 1 -> 2
238        // Bit 1: 1 -> 3
239        // Bit 0: 2 -> 3
240        let (and_mask, or_mask) = match way_idx {
241            // Clear bits 5-3
242            0 => (!0b111000, 0b000000),
243            // Clear bits 2-1 and set bit 5
244            1 => (!0b000110, 0b100000),
245            // Clear bit 0 and set bits 4 and 2
246            2 => (!0b000001, 0b010100),
247            // Set bits 3, 1, and 0
248            3 => (!0b000000, 0b001011),
249            _ => panic!("Invalid way index, should be 0-3: {way_idx}"),
250        };
251
252        self.lru_bits[entry_idx] &= and_mask;
253        self.lru_bits[entry_idx] |= or_mask;
254    }
255
256    pub fn write_through_u8(&mut self, address: u32, value: u8) {
257        self.cache_write_through(address, move |cache, way_idx, entry_idx| {
258            let address = cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xF);
259            if !address.bit(0) {
260                cache.ram[address >> 1].set_msb(value);
261            } else {
262                cache.ram[address >> 1].set_lsb(value);
263            }
264        });
265    }
266
267    pub fn write_through_u16(&mut self, address: u32, value: u16) {
268        self.cache_write_through(address, move |cache, way_idx, entry_idx| {
269            let address = cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xE);
270            cache.ram[address >> 1] = value;
271        });
272    }
273
274    pub fn write_through_u32(&mut self, address: u32, value: u32) {
275        self.cache_write_through(address, move |cache, way_idx, entry_idx| {
276            let address = (cache_ram_addr(way_idx, entry_idx) | ((address as usize) & 0xC)) >> 1;
277            cache.ram[address] = (value >> 16) as u16;
278            cache.ram[address + 1] = value as u16;
279        });
280    }
281
282    fn cache_write_through(&mut self, address: u32, set_fn: impl FnOnce(&mut Self, usize, usize)) {
283        if !self.control.cache_enabled {
284            return;
285        }
286
287        let entry_idx = cache_entry_index(address);
288        let tag = tag_address(address);
289
290        // Iterate in reverse for slightly better performance when cache is in 2-way mode
291        for way_idx in (0..4).rev() {
292            if self.ways[way_idx].valid_bits.bit(entry_idx as u8)
293                && self.ways[way_idx].tags[entry_idx] == tag
294            {
295                self.update_lru_bits(way_idx, entry_idx);
296                set_fn(self, way_idx, entry_idx);
297                return;
298            }
299        }
300    }
301
302    // $FFFFFE92: CCR (Cache control register)
303    pub fn read_control(&self) -> u8 {
304        self.control.read()
305    }
306
307    // $FFFFFE92: CCR (Cache control register)
308    pub fn write_control(&mut self, value: u8) {
309        self.control.write(value);
310
311        if value.bit(4) {
312            self.purge_all();
313        }
314    }
315
316    pub fn purge_all(&mut self) {
317        for way in self.ways.as_mut() {
318            way.valid_bits = 0;
319        }
320
321        self.lru_bits.fill(0);
322    }
323
324    // A29-31 = 010
325    pub fn associative_purge(&mut self, address: u32) {
326        // Invalidates a single cache line
327        let idx = cache_entry_index(address);
328        let mask = !(1 << idx);
329        for way in self.ways.as_mut() {
330            way.valid_bits &= mask;
331        }
332
333        // TODO should associative purge clear the LRU bits?
334        self.lru_bits[idx] = 0;
335    }
336
337    // A29-31 = 011
338    pub fn read_address_array(&self, address: u32) -> u32 {
339        let entry_idx = cache_entry_index(address);
340        let way_idx = self.control.way as usize;
341
342        (u32::from(self.ways[way_idx].valid_bits.bit(entry_idx as u8)) << 1)
343            | (u32::from(self.lru_bits[entry_idx]) << 3)
344            | (self.ways[way_idx].tags[entry_idx] << 10)
345    }
346
347    // A29-31 = 011
348    pub fn write_address_array(&mut self, address: u32, value: u32) {
349        let entry_idx = cache_entry_index(address);
350        let way_idx = self.control.way as usize;
351        let tag = tag_address(address);
352        let valid = address.bit(1);
353        let lru_bits = ((value >> 3) & 0x3F) as u8;
354
355        if valid {
356            self.ways[way_idx].valid_bits |= 1 << entry_idx;
357        } else {
358            self.ways[way_idx].valid_bits &= !(1 << entry_idx);
359        }
360
361        self.ways[way_idx].tags[entry_idx] = tag;
362        self.lru_bits[entry_idx] = lru_bits;
363    }
364
365    // A29-31 = 110
366    pub fn read_data_array_u8(&self, address: u32) -> u8 {
367        let word = self.ram[((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1)];
368        word.to_be_bytes()[(address & 1) as usize]
369    }
370
371    // A29-31 = 110
372    pub fn read_data_array_u16(&self, address: u32) -> u16 {
373        self.ram[((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1)]
374    }
375
376    // A29-31 = 110
377    pub fn read_data_array_u32(&self, address: u32) -> u32 {
378        let address = ((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1) & !1;
379        let high_word = self.ram[address];
380        let low_word = self.ram[address + 1];
381        (u32::from(high_word) << 16) | u32::from(low_word)
382    }
383
384    // A29-31 = 110
385    pub fn write_data_array_u8(&mut self, address: u32, value: u8) {
386        let word_addr = ((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1);
387        if !address.bit(0) {
388            self.ram[word_addr].set_msb(value);
389        } else {
390            self.ram[word_addr].set_lsb(value);
391        }
392    }
393
394    // A29-31 = 110
395    pub fn write_data_array_u16(&mut self, address: u32, value: u16) {
396        self.ram[((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1)] = value;
397    }
398
399    // A29-31 = 110
400    pub fn write_data_array_u32(&mut self, address: u32, value: u32) {
401        let address = ((address >> 1) as usize) & (CACHE_RAM_LEN_WORDS - 1) & !1;
402        self.ram[address] = (value >> 16) as u16;
403        self.ram[address + 1] = value as u16;
404    }
405
406    pub fn debug_view(&mut self) -> impl DebugMemoryView {
407        DebugWordsView(self.ram.as_mut_slice(), Endian::Big)
408    }
409
410    pub(crate) fn debug_state(&self) -> CacheDebugState {
411        CacheDebugState {
412            enabled: self.control.cache_enabled,
413            instruction_replacement_enabled: !self.control.disable_instruction_replacement,
414            data_replacement_enabled: !self.control.disable_data_replacement,
415            mode: self.control.mode,
416        }
417    }
418}
419
420#[inline(always)]
421fn cache_ram_addr(way_idx: usize, entry_idx: usize) -> usize {
422    (way_idx << 10) | (entry_idx << 4)
423}
424
425#[inline(always)]
426fn cache_entry_index(address: u32) -> usize {
427    // Cache is indexed using address bits 4-9
428    ((address as usize) >> 4) & 0x3F
429}
430
431#[inline(always)]
432fn tag_address(address: u32) -> u32 {
433    // Cache entries are tagged using address bits 10-28
434    (address & 0x1FFFFFFF) >> 10
435}