1//! SH7604 CPU cache 2//! 3//! This is a 4-way set-associative cache that uses a pseudo-LRU algorithm for cache replacement. 4//! 5//! Cache replacement is performed when a cached read misses. The cache is write-through, so writes 6//! will only update cache if there is a cache hit. 7//! 8//! Games that are known to depend on CPU cache emulation (specifically data cache): 9//! * WWF Raw (32X) writes to cartridge ROM addresses and expects to be able to read back the written 10//! values from CPU cache. Without cache, it won't correctly populate 32X palette RAM which causes 11//! missing graphics in menus. 12//! * Pitfall: The Mayan Adventure (32X) writes to addresses around $00090000 (out-of-bounds in boot 13//! ROM area) and expects to be able to read back the written values from CPU cache. Without cache, 14//! objects/"sprites" that are partially offscreen will not display at all until they are entirely 15//! onscreen. 16 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; 23 24const CACHE_RAM_LEN_WORDS: usize = 4 * 1024 / 2; 25 26const WAYS: usize = 4; 27 28// Cache lines are 16 bytes and there are 4 ways in each cache line 29// 4096 / 16 / 4 = 64 30const CACHE_ENTRIES: usize = 64; 31 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}