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.
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}