From ef78fba7e5b9832e23c40805794768f23b61e978 Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 21:24:06 -0400 Subject: [PATCH 1/9] cpu: make the CPU model a type, not a build flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R4400 vs R5000 was 99 `#[cfg(feature = "r5k")]` sites, 88 of them inside mips_cache_v2.rs, plus 54 `mips4` sites gating the ISA. That forced a whole rebuild per CPU — cpu-tests/run/matrix.sh and `iris-bench matrix` each shell out to cargo once per cell — and it kept two models from ever being compared in one process. The geometry is now const-generic and each model is a type alias: pub type R4400Cache = CpuCache<16384,16,1,1024, ..., false,0x0440,0x0500,48>; pub type R5000Cache = CpuCache<32768,32,2,1024, ..., true, 0x2321,0x2300,48>; Every `#[cfg]` on the CPU axis becomes `if Self::IS_R5K` / `if C::MIPS4` on a const, so it folds at monomorphisation rather than costing a branch. Verified in the disassembly: an L1 index is `shr $4; and $1023` on R4400 and `shr $5; and $511` on R5000, with the discriminator absent from both — the same code the cfg build emitted. Model identity that is not cache behaviour lives on its own trait, so MipsCache stays about caching: pub trait CpuModel: MipsCache { const MIPS4: bool; const PRID: u32; const FIR: u32; const TLB_ENTRIES: usize; const NAME: &'static str; } That retires the PRId/FIR cfgs in MipsCore::power_on and the CP0 Config geometry constants in mips_exec.rs, both of which now come from the model. mips_core.rs and mips_exec.rs are at zero CPU-feature cfgs. Tests 407 -> 419 on a default build. The increase is coverage, not inflation: the L1 stress tests are generic over the model and run against both, and the 10 MIPS IV tests select a MIPS IV model (PassthroughCacheM4) instead of needing --features mips4, so MIPS IV correctness is checked in every build. No throughput regression. Interleaved A/B of baseline vs this on the same machine state, 3 rounds each: +0.73% mean on r4400-jitv2, inside a 4.5% run-to-run spread, 40/40 accuracy throughout. r4400-interp is +3.7% against a 0.2% noise floor. Machine still selects the model by cargo feature; making that a runtime choice is the next step and is why the concrete type appears in exactly one place there. Co-Authored-By: Claude Opus 5 (1M context) --- src/machine.rs | 12 +- src/mips_cache_v2.rs | 1066 ++++++++++++++++++++++------------------- src/mips_core.rs | 15 +- src/mips_exec.rs | 209 ++++---- src/mips_exec_test.rs | 55 ++- 5 files changed, 704 insertions(+), 653 deletions(-) diff --git a/src/machine.rs b/src/machine.rs index a182d1e..f4278f7 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -26,7 +26,11 @@ use crate::mc::MemoryController; use crate::mips_tlb::MipsTlb; use crate::mips_exec::{MipsExecutor, MipsCpu, MipsCpuConfig, MipsCpuDebugAdapter}; use crate::gdb_stub::CpuDebug; -use crate::mips_cache_v2::R4000Cache; +// Step 1 keeps the cargo feature as the selector; step 3 makes this a runtime choice. +#[cfg(not(feature = "r5k"))] +use crate::mips_cache_v2::R4400Cache as SelectedCache; +#[cfg(feature = "r5k")] +use crate::mips_cache_v2::R5000Cache as SelectedCache; use crate::hpc3::Hpc3; use crate::ioc::{Ioc, GioSlot, GIO_SLOT_MAP, profile_idx}; use crate::monitor::Monitor; @@ -60,7 +64,7 @@ pub fn emulator_name() -> &'static str { } pub struct Machine { - cpu: Arc>, + cpu: Arc>, _phys: Arc, // Keep reference to Physical Bus mc: MemoryController, hpc3: Hpc3, @@ -272,7 +276,7 @@ impl Machine { // r5ksc_triton: Triton reports L2 size via CONFIG_TR_SS — EEPROM word left 0. // r5k without r5ksc: no L2 — leave 0 so PROM sees no secondary cache. #[cfg(all(feature = "r5ksc", not(feature = "r5ksc_triton")))] - eeprom_mc.lock().set_cachsz((crate::mips_cache_v2::L2_SIZE / 4096) as u16); + eeprom_mc.lock().set_cachsz((::L2_SIZE / 4096) as u16); #[cfg(all(feature = "r5k", not(feature = "r5ksc")))] eeprom_mc.lock().set_cachsz(0); @@ -699,7 +703,7 @@ impl Machine { let cfg = MipsCpuConfig::indy(); let tlb = MipsTlb::new(cfg.tlb_entries); let sysad: Arc = phys.clone(); - let mut executor: MipsExecutor = MipsExecutor::new(sysad, tlb, &cfg); + let mut executor: MipsExecutor = MipsExecutor::new(sysad, tlb, &cfg); // Load default symbol maps if they exist { diff --git a/src/mips_cache_v2.rs b/src/mips_cache_v2.rs index 7bd337d..b682a2c 100644 --- a/src/mips_cache_v2.rs +++ b/src/mips_cache_v2.rs @@ -53,65 +53,8 @@ const fn ctz(n: usize) -> u32 { #[repr(u8)] enum CacheKind { Insn = 0, Data = 1, L2 = 2 } -// Cache sizes and line sizes — exported so mips_exec can build CP0 Config. -// R5K uses larger 2-way L1 caches; L2 geometry is the same for both CPUs. -// For R5K, IC_SIZE/DC_SIZE are the TOTAL (both ways combined) cache sizes. -// IC_WAYS/DC_WAYS control 2-way behaviour; all downstream constants are derived. - -// R4400 (default) configuration -#[cfg(not(feature = "r5k"))] -pub const IC_SIZE: usize = 16 * 1024; // 16 KB L1 instruction cache -#[cfg(not(feature = "r5k"))] -pub const IC_LINE: usize = 16; // 16-byte lines -#[cfg(not(feature = "r5k"))] -pub const DC_SIZE: usize = 16 * 1024; // 16 KB L1 data cache -#[cfg(not(feature = "r5k"))] -pub const DC_LINE: usize = 16; // 16-byte lines -#[cfg(not(feature = "r5k"))] -pub const IC_WAYS: usize = 1; -#[cfg(not(feature = "r5k"))] -pub const DC_WAYS: usize = 1; - -// R5000 configuration (2-way, larger L1, same L2) -#[cfg(feature = "r5k")] -pub const IC_SIZE: usize = 32 * 1024; // 32 KB L1 instruction cache (both ways) -#[cfg(feature = "r5k")] -pub const IC_LINE: usize = 32; // 32-byte lines -#[cfg(feature = "r5k")] -pub const DC_SIZE: usize = 32 * 1024; // 32 KB L1 data cache (both ways) -#[cfg(feature = "r5k")] -pub const DC_LINE: usize = 32; -#[cfg(feature = "r5k")] -pub const IC_WAYS: usize = 2; -#[cfg(feature = "r5k")] -pub const DC_WAYS: usize = 2; - -// Number of sets per way (= NUM_LINES / WAYS) -pub const IC_NUM_SETS: usize = IC_SIZE / IC_LINE / IC_WAYS; -pub const DC_NUM_SETS: usize = DC_SIZE / DC_LINE / DC_WAYS; - -// L2 present only when r5ksc (or r4k which always has L2). -// r5ksc without r5ksc_triton = external R4600SC-style cache (same geometry, no SE/SS). -// r5ksc_triton = Triton on-die L2 (SE enable, SS size bits, SC=0 in config). -// -// When r5k is on but r5ksc is off, L2_SIZE=0 meaning no L2. L2Cache is still instantiated -// with 1 dummy line so const generics don't underflow; l2_active() returns false so it is -// never actually accessed. -#[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] -pub const L2_SIZE: usize = 1024 * 1024; // R4K or r5k+r5ksc: 1 MB unified L2 -#[cfg(all(feature = "r5k", not(feature = "r5ksc")))] -pub const L2_SIZE: usize = 0; // R5K without secondary cache: logically absent - -#[cfg(not(feature = "r5k"))] -pub const L2_LINE: usize = 128; // R4K: 128-byte lines -#[cfg(all(feature = "r5k", not(feature = "r5ksc")))] -pub const L2_LINE: usize = 128; // dummy (L2_SIZE=0, never accessed) -#[cfg(all(feature = "r5k", feature = "r5ksc"))] -pub const L2_LINE: usize = 32; // R5K/Triton: 32-byte lines - -// Effective L2 size for the Cache<> generic: at least LINE so NUM_LINES >= 1. -// When L2_SIZE=0 (r5k without r5ksc) this gives a 1-line dummy; l2_active() guards all access. -pub const L2_CACHE_SIZE: usize = if L2_SIZE == 0 { L2_LINE } else { L2_SIZE }; +// Cache geometry is const-generic, not a cargo feature: each CPU model is its own +// monomorphisation, so every index/mask below still folds to a literal. // Re-export cache operation constants for convenience pub use crate::mips_isa::{ @@ -385,7 +328,32 @@ impl From for u32 { fn from(t: L2Tag) -> Self { t.0 } } /// - Data read/write through L1-D cache (VIPT) /// - Cache operations for CACHE instruction support /// - Load-Linked / Store-Conditional support +/// Per-model settings that are not cache behaviour: ISA level, CP0/CP1 identity, TLB size. +/// Const, so every use folds at monomorphisation instead of costing a runtime check. +pub trait CpuModel: MipsCache { + /// MIPS IV opcodes decode rather than raising Reserved Instruction. + const MIPS4: bool; + /// CP0 PRId reset value. + const PRID: u32; + /// CP1 FIR reset value. + const FIR: u32; + /// JTLB entries. + const TLB_ENTRIES: usize; + /// Name as the guest and the benchmark report see it. + const NAME: &'static str; +} + pub trait MipsCache: Send + Sync { + /// L1/L2 geometry, so CP0 Config reports this model rather than a build-time constant. + const IC_SIZE: usize; + const IC_LINE: usize; + const IC_WAYS: usize; + const DC_SIZE: usize; + const DC_LINE: usize; + const DC_WAYS: usize; + const L2_SIZE: usize; + const L2_LINE: usize; + /// Fetch instruction from L1 instruction cache. /// Returns `FetchInstrResult::hit(ptr)` on success, `FetchInstrResult::exception(status)` on error. /// The caller must call `decode_into` on the slot before use. @@ -479,7 +447,7 @@ pub trait MipsCache: Send + Sync { /// Passthrough cache that performs no caching - all accesses go directly to memory /// Useful for testing and debugging -pub struct PassthroughCache { +pub struct PassthroughCacheOf { downstream: Arc, llbit: UnsafeCell, lladdr: UnsafeCell, @@ -488,10 +456,15 @@ pub struct PassthroughCache { } // Safety: Single-threaded access only (CPU thread) -unsafe impl Send for PassthroughCache {} -unsafe impl Sync for PassthroughCache {} +unsafe impl Send for PassthroughCacheOf {} +unsafe impl Sync for PassthroughCacheOf {} + +/// MIPS III passthrough — the default for tests that do not care about ISA level. +pub type PassthroughCache = PassthroughCacheOf; +/// MIPS IV passthrough, for tests that exercise MIPS IV opcodes. +pub type PassthroughCacheM4 = PassthroughCacheOf; -impl PassthroughCache { +impl PassthroughCacheOf { pub fn new(downstream: Arc) -> Self { Self { downstream, @@ -502,13 +475,30 @@ impl PassthroughCache { } } -impl From> for PassthroughCache { +impl From> for PassthroughCacheOf { fn from(downstream: Arc) -> Self { Self::new(downstream) } } -impl MipsCache for PassthroughCache { +impl CpuModel for PassthroughCacheOf { + const MIPS4: bool = MIPS4; + const PRID: u32 = if MIPS4 { 0x0000_2321 } else { 0x0000_0440 }; + const FIR: u32 = if MIPS4 { 0x0000_2300 } else { 0x0000_0500 }; + const TLB_ENTRIES: usize = 48; + const NAME: &'static str = "passthrough"; +} + +impl MipsCache for PassthroughCacheOf { + const IC_SIZE: usize = 0; + const IC_LINE: usize = 0; + const IC_WAYS: usize = 1; + const DC_SIZE: usize = 0; + const DC_LINE: usize = 0; + const DC_WAYS: usize = 1; + const L2_SIZE: usize = 0; + const L2_LINE: usize = 0; + fn fetch(&self, _virt_addr: u64, phys_addr: u64) -> FetchInstrResult { let r = self.downstream.read32(phys_addr as u32); if r.is_ok() { @@ -626,7 +616,7 @@ impl CacheVec { /// way0 at [0..NUM_LINES), way1 at [NUM_LINES..2*NUM_LINES), etc. /// `TAGS` must equal `NUM_LINES * WAYS`; `DATA` = `SIZE / 8` (all ways). struct Cache { + const TAGS: usize, const DATA: usize, const NINSTRS: usize> { /// Heap-allocated typed tag array — TAGS entries (NUM_LINES * WAYS). tags: UnsafeCell>, /// Heap-allocated data array — entire cache contents as u64 chunks (all ways). @@ -638,12 +628,12 @@ struct Cache Send for Cache {} + const TAGS: usize, const DATA: usize, const NINSTRS: usize> Send for Cache {} unsafe impl Sync for Cache {} + const TAGS: usize, const DATA: usize, const NINSTRS: usize> Sync for Cache {} impl Cache { + const TAGS: usize, const DATA: usize, const NINSTRS: usize> Cache { // ---- Compile-time geometry constants ---- /// Number of sets = SIZE / LINE / WAYS. get_index() returns values in [0, NUM_LINES). const NUM_LINES: usize = SIZE / LINE / WAYS; @@ -661,14 +651,8 @@ impl Self { - // Allocate L2 decoded-instruction slots for L2 caches only. - // R5K always gets an empty vec (L1I fills its own ic_instrs from l2.data at fill time). - let instrs: Vec = { - #[cfg(not(feature = "r5k"))] - { if KIND == CacheKind::L2 as u8 { (0..SIZE / 4).map(|_| DecodedInstr::default()).collect() } else { Vec::new() } } - #[cfg(feature = "r5k")] - { Vec::new() } - }; + // NINSTRS = decoded-instruction slots this level owns (0 when it owns none). + let instrs: Vec = (0..NINSTRS).map(|_| DecodedInstr::default()).collect(); Self { tags: UnsafeCell::new(vec![TAG::default(); TAGS].into_boxed_slice()), // SAFETY: u64 is valid at all-zero bit patterns. Box::new_zeroed avoids @@ -812,17 +796,23 @@ const DEBUG_TRACK_ADDR: Option = None; /// /// This implementation keeps L1-I, L1-D, and L2 in a single object /// so that L2 evictions can invalidate L1 lines as needed. -pub struct R4000Cache { +pub struct CpuCache< + const IC_SIZE: usize, const IC_LINE: usize, const IC_WAYS: usize, const IC_TAGS: usize, + const DC_SIZE: usize, const DC_LINE: usize, const DC_WAYS: usize, const DC_TAGS: usize, const DC_DATA: usize, + const L2_CACHE_SIZE: usize, const L2_LINE: usize, const L2_TAGS: usize, const L2_DATA: usize, + const L2_NINSTRS: usize, const HAS_L2: bool, + const MIPS4: bool, const PRID: u32, const FIR: u32, const TLB_ENTRIES: usize, +> { downstream: Arc, // L1 Instruction Cache (16 KB, 16-byte lines) - ic: ICache, + ic: ICacheT, // L1 Data Cache (16 KB, 16-byte lines) - dc: DCache, + dc: DCacheT, // L2 Unified Cache (1 MB, 128-byte lines) - l2: L2Cache, + l2: L2CacheT, // Load-Linked / Store-Conditional support llbit: UnsafeCell, @@ -838,18 +828,10 @@ pub struct R4000Cache { #[cfg(feature = "r5ksc_triton")] l2_enabled: bool, - // R5K: L1I owns its own decoded-instruction slots (L2 non-inclusive). - // Indexed as: ic_instrs[way * IC_NUM_SETS * INSTRS_PER_LINE + set_idx * INSTRS_PER_LINE + word] - // R5K has 2 ways; total: 2 × IC_NUM_SETS sets × (IC_LINE/4) instrs/line. - #[cfg(feature = "r5k")] + // 2-way L1I owns its decode slots (non-inclusive L2); empty on a direct-mapped model. ic_instrs: CacheVec, - - // R5K only: LRU bit per set for L1I and L1D, packed as a bitmap. - // Bit N = 0: way0 is LRU (fill way0 next); bit N = 1: way1 is LRU. - // 512 sets → 8 × u64, fits in one cache line. - #[cfg(feature = "r5k")] + // Per-set LRU bitmaps for 2-way L1I/L1D; empty on a direct-mapped model. ic_lru: UnsafeCell>, - #[cfg(feature = "r5k")] dc_lru: UnsafeCell>, // Debug tracking - cache line boundaries and indices for tracked address @@ -869,29 +851,93 @@ pub struct R4000Cache { debug_companion_l2_idx: usize, } -unsafe impl Send for R4000Cache {} -unsafe impl Sync for R4000Cache {} - -// Type aliases for the concrete cache instances, for brevity in R4000Cache impls. -// TAGS = (SIZE/LINE) = NUM_SETS * WAYS (tag array spans all ways). -// DATA = SIZE/8 (all ways combined). ICache DATA=0: fetch() indexes l2.instrs or ic_instrs. -// get_index() returns a set index in [0, NUM_LINES); way1 at set_idx + NUM_LINES. -type ICache = Cache; -type DCache = Cache; -type L2Cache = Cache; +unsafe impl Send for CpuCache {} +unsafe impl Sync for CpuCache {} + +// Per-level cache types, parameterised so each CPU model monomorphises its own. +type ICacheT = + Cache; +type DCacheT = + Cache; +type L2CacheT = + Cache; + +/// SGI Indy R4400: direct-mapped 16K L1s, 1 MB unified L2 owning the decode slots. +pub type R4400Cache = CpuCache<16384, 16, 1, 1024, + 16384, 16, 1, 1024, 2048, + 1048576, 128, 8192, 131072, 262144, true, + false, 0x0000_0440, 0x0000_0500, 48>; +/// SGI Indy R5000: 2-way 32K L1s, no secondary cache; L1I owns its decode slots. +pub type R5000Cache = CpuCache<32768, 32, 2, 1024, + 32768, 32, 2, 1024, 4096, + 128, 128, 1, 16, 0, false, + true, 0x0000_2321, 0x0000_2300, 48>; + +impl CpuCache { + // Model discriminator: folds to a literal, so it replaces #[cfg(feature = "r5k")]. + const IS_R5K: bool = IC_WAYS == 2; + // Logical L2 size; 0 means the model has no secondary cache. + pub const L2_SIZE: usize = if HAS_L2 { L2_CACHE_SIZE } else { 0 }; + + const IC_NUM_SETS: usize = IC_SIZE / IC_LINE / IC_WAYS; + const DC_NUM_SETS: usize = DC_SIZE / DC_LINE / DC_WAYS; + + const IC_LINE_SHIFT: u32 = IC_LINE.trailing_zeros(); + const IC_LINE_MASK: usize = IC_LINE - 1; + const IC_NUM_LINES: usize = Self::IC_NUM_SETS; + const IC_NUM_LINES_SHIFT: u32 = Self::IC_NUM_SETS.trailing_zeros(); + const IC_NUM_LINES_MASK: usize = Self::IC_NUM_SETS - 1; + const IC_INSTRS_PER_LINE: usize = IC_LINE / 4; + const IC_INSTR_SHIFT: u32 = Self::IC_LINE_SHIFT - 2; + const IC_INSTR_MASK: usize = Self::IC_INSTRS_PER_LINE - 1; + const IC_CHUNKS_PER_LINE: usize = IC_LINE / 8; + const IC_CHUNKS_PER_LINE_SHIFT: u32 = Self::IC_LINE_SHIFT - 3; + + const DC_LINE_SHIFT: u32 = DC_LINE.trailing_zeros(); + const DC_LINE_MASK: usize = DC_LINE - 1; + const DC_NUM_LINES: usize = Self::DC_NUM_SETS; + const DC_NUM_LINES_SHIFT: u32 = Self::DC_NUM_SETS.trailing_zeros(); + const DC_NUM_LINES_MASK: usize = Self::DC_NUM_SETS - 1; + const DC_INSTRS_PER_LINE: usize = DC_LINE / 4; + const DC_INSTR_SHIFT: u32 = Self::DC_LINE_SHIFT - 2; + const DC_INSTR_MASK: usize = Self::DC_INSTRS_PER_LINE - 1; + const DC_CHUNKS_PER_LINE: usize = DC_LINE / 8; + const DC_CHUNKS_PER_LINE_SHIFT: u32 = Self::DC_LINE_SHIFT - 3; + + const L2_LINE_SHIFT: u32 = L2_LINE.trailing_zeros(); + const L2_LINE_MASK: usize = L2_LINE - 1; + const L2_NUM_LINES: usize = L2_CACHE_SIZE / L2_LINE; + const L2_NUM_LINES_SHIFT: u32 = (L2_CACHE_SIZE / L2_LINE).trailing_zeros(); + const L2_NUM_LINES_MASK: usize = (L2_CACHE_SIZE / L2_LINE) - 1; + const L2_INSTRS_PER_LINE: usize = L2_LINE / 4; + const L2_INSTR_SHIFT: u32 = Self::L2_LINE_SHIFT - 2; + const L2_INSTR_MASK: usize = Self::L2_INSTRS_PER_LINE - 1; + const L2_CHUNKS_PER_LINE: usize = L2_LINE / 8; + const L2_CHUNKS_PER_LINE_SHIFT: u32 = Self::L2_LINE_SHIFT - 3; -impl R4000Cache { pub fn new(downstream: Arc) -> Self { - let ic = ICache::new(); - let dc = DCache::new(); - let l2 = L2Cache::new(); + let ic = ICacheT::::new(); + let dc = DCacheT::::new(); + let l2 = L2CacheT::::new(); #[cfg(feature = "debug_cache")] let (debug_l1d_line, debug_l2_line, debug_companion_l1d_line, debug_companion_l2_line, debug_l1d_idx, debug_l2_idx, debug_companion_l2_idx) = { if let Some(addr) = DEBUG_TRACK_ADDR { - let l1_line_mask = DCache::LINE_MASK as u64; - let l2_line_mask = L2Cache::LINE_MASK as u64; + let l1_line_mask = Self::DC_LINE_MASK as u64; + let l2_line_mask = Self::L2_LINE_MASK as u64; let companion_addr = addr ^ 0x00400000; // XOR with COMPANION_BIT let target_l1d_line = addr & !l1_line_mask; @@ -928,16 +974,15 @@ impl R4000Cache { l1i_fetch_count: Arc::new(AtomicU64::new(0)), #[cfg(feature = "r5ksc_triton")] l2_enabled: false, // starts disabled; PROM enables via CONFIG_SE - // R5K: 2-way ic_instrs (2 × IC_NUM_SETS × IC_LINE/4 words). - #[cfg(feature = "r5k")] - ic_instrs: CacheVec::new( - (0..IC_WAYS * IC_NUM_SETS * (IC_LINE / 4)) + // Const-guarded: a direct-mapped model allocates none of these. + ic_instrs: CacheVec::new(if Self::IS_R5K { + (0..IC_WAYS * Self::IC_NUM_SETS * (IC_LINE / 4)) .map(|_| DecodedInstr::default()).collect() - ), - #[cfg(feature = "r5k")] - ic_lru: UnsafeCell::new(vec![0u64; IC_NUM_SETS.div_ceil(64)].into_boxed_slice()), - #[cfg(feature = "r5k")] - dc_lru: UnsafeCell::new(vec![0u64; DC_NUM_SETS.div_ceil(64)].into_boxed_slice()), + } else { Vec::new() }), + ic_lru: UnsafeCell::new( + if Self::IS_R5K { vec![0u64; Self::IC_NUM_SETS.div_ceil(64)] } else { Vec::new() }.into_boxed_slice()), + dc_lru: UnsafeCell::new( + if Self::IS_R5K { vec![0u64; Self::DC_NUM_SETS.div_ceil(64)] } else { Vec::new() }.into_boxed_slice()), #[cfg(feature = "debug_cache")] debug_l1d_line, #[cfg(feature = "debug_cache")] @@ -957,19 +1002,27 @@ impl R4000Cache { } -impl From> for R4000Cache { +impl From> for CpuCache { fn from(downstream: Arc) -> Self { Self::new(downstream) } } -impl R4000Cache { +impl CpuCache { /// Check if we're tracking this physical address (for debug purposes) #[cfg(feature = "debug_cache")] #[inline] fn is_tracking_l1d(&self, phys_addr: u64) -> bool { DEBUG_TRACK_ADDR.is_some() && { - let line = phys_addr & !(DCache::LINE_MASK as u64); + let line = phys_addr & !(Self::DC_LINE_MASK as u64); line == self.debug_l1d_line || line == self.debug_companion_l1d_line } } @@ -978,7 +1031,7 @@ impl R4000Cache { #[inline] fn is_tracking_l2(&self, phys_addr: u64) -> bool { DEBUG_TRACK_ADDR.is_some() && { - let line = phys_addr & !(L2Cache::LINE_MASK as u64); + let line = phys_addr & !(Self::L2_LINE_MASK as u64); line == self.debug_l2_line || line == self.debug_companion_l2_line } } @@ -1000,7 +1053,7 @@ impl R4000Cache { fn is_tracking_addr(&self, virt_addr: u64, phys_addr: u64) -> bool { DEBUG_TRACK_ADDR.is_some() && { // Check if the physical line matches (most reliable) - let line = phys_addr & !(DCache::LINE_MASK as u64); + let line = phys_addr & !(Self::DC_LINE_MASK as u64); if line == self.debug_l1d_line || line == self.debug_companion_l1d_line { return true; } @@ -1019,7 +1072,7 @@ impl R4000Cache { #[cfg(feature = "debug_cache")] #[inline] fn tracking_label(&self, phys_addr: u64) -> &'static str { - let line = phys_addr & !(DCache::LINE_MASK as u64); + let line = phys_addr & !(Self::DC_LINE_MASK as u64); if line == self.debug_l1d_line { "TARGET" } else if line == self.debug_companion_l1d_line { @@ -1042,16 +1095,12 @@ impl R4000Cache { } /// Returns whether L2 is currently usable. - /// - R4K / r5ksc (external): always true when L2_SIZE > 0. + /// - R4K / r5ksc (external): always true when Self::L2_SIZE > 0. /// - r5ksc_triton: gated by CONFIG_SE (l2_enabled field). /// - r5k without r5ksc: always false (no L2). #[inline] fn l2_active(&self) -> bool { - if L2_SIZE == 0 { return false; } - #[cfg(feature = "r5ksc_triton")] - { self.l2_enabled } - #[cfg(not(feature = "r5ksc_triton"))] - { true } + HAS_L2 } /// Triton only: set L2 enable state from CONFIG_SE. On off→on transition, invalidate @@ -1061,7 +1110,7 @@ impl R4000Cache { let was = self.l2_enabled; self.l2_enabled = enabled; if enabled && !was { - for idx in 0..L2Cache::NUM_LINES { + for idx in 0..Self::L2_NUM_LINES { self.l2.set_tag(idx, L2Tag::default()); } } @@ -1074,7 +1123,6 @@ impl R4000Cache { } /// Extract virtual index bits [14:12] for L2 PIdx field - #[cfg(feature = "r5k")] #[inline(always)] unsafe fn lru_get(bm: *const Box<[u64]>, set: usize) -> bool { let slice: &[u64] = &*bm; @@ -1082,7 +1130,6 @@ impl R4000Cache { } /// Set or clear LRU bit for `set` in a packed u64 bitmap. - #[cfg(feature = "r5k")] #[inline(always)] unsafe fn lru_set(bm: *mut Box<[u64]>, set: usize, val: usize) { let slice: &mut [u64] = &mut *bm; @@ -1109,16 +1156,16 @@ impl R4000Cache { /// L2 line. The caller iterates over `l1_lines_per_l2` indices starting here, /// stepping by 1 (indices wrap naturally via the cache mask). #[inline] - fn l2_idx_to_l1_base_idx( - &self, l2_idx: usize, pidx: u32, _l1: &Cache + fn l2_idx_to_l1_base_idx( + &self, l2_idx: usize, pidx: u32, _l1: &Cache ) -> usize { // Physical bits of the L2 line start address that are below bit 12 (page boundary) // These bits are the same in VA and PA, so we can derive them from the L2 index. - let phys_sub_bits = (l2_idx << L2Cache::LINE_SHIFT as usize) & 0xFFF; + let phys_sub_bits = (l2_idx << Self::L2_LINE_SHIFT as usize) & 0xFFF; // Reconstruct the virtual address bits used for L1 indexing let virt_index_bits = ((pidx as usize) << L2_PIDX_VADDR_SHIFT as usize) | phys_sub_bits; - (virt_index_bits >> Cache::::LINE_SHIFT as usize) - & Cache::::NUM_LINES_MASK + (virt_index_bits >> Cache::::LINE_SHIFT as usize) + & Cache::::NUM_LINES_MASK } /// Check if the given physical address overlaps with the Load Linked address. @@ -1145,7 +1192,7 @@ impl R4000Cache { #[cfg(feature = "debug_cache")] if tag.is_valid() { - let phys_addr = l1_tag_to_phys(tag, (idx << ICache::LINE_SHIFT) as u64); + let phys_addr = l1_tag_to_phys(tag, (idx << Self::IC_LINE_SHIFT) as u64); if self.is_tracking_l1d(phys_addr) { println!("[CACHE DEBUG] invalidate_l1i_line: {} idx=0x{:x}, phys_addr=0x{:08x}, ptag=0x{:010x}", self.tracking_label(phys_addr), idx, phys_addr, tag.line_addr()); @@ -1153,7 +1200,7 @@ impl R4000Cache { } if cascade && tag.is_valid() { - let phys_addr = l1_tag_to_phys(tag, (idx << ICache::LINE_SHIFT) as u64); + let phys_addr = l1_tag_to_phys(tag, (idx << Self::IC_LINE_SHIFT) as u64); self.invalidate_l2_line_phys(phys_addr); } @@ -1169,7 +1216,7 @@ impl R4000Cache { #[cfg(feature = "debug_cache")] if self.is_tracking_l1d_idx(idx) { if tag.cs != L1D_CS_INVALID as u8 { - let phys_addr = l1d_tag_to_phys(tag, (idx << DCache::LINE_SHIFT) as u64); + let phys_addr = l1d_tag_to_phys(tag, (idx << Self::DC_LINE_SHIFT) as u64); println!("[CACHE DEBUG] invalidate_l1d_line: {} idx=0x{:x}, phys_addr=0x{:08x}, ptag=0x{:010x}, cs={}, coherent={}", self.tracking_label(phys_addr), idx, phys_addr, tag.line_addr(), tag.cs, coherent); } else { @@ -1180,12 +1227,12 @@ impl R4000Cache { // Only clear llbit for software-initiated coherency invalidations, not hardware fills. // On a uniprocessor R4000 there are no external snoops; llbit survives capacity evictions. if coherent && tag.cs != L1D_CS_INVALID as u8 { - let phys_addr = l1d_tag_to_phys(tag, (idx << DCache::LINE_SHIFT) as u64); - self.check_and_clear_llbit(phys_addr, DCache::LINE_MASK); + let phys_addr = l1d_tag_to_phys(tag, (idx << Self::DC_LINE_SHIFT) as u64); + self.check_and_clear_llbit(phys_addr, Self::DC_LINE_MASK); } if cascade && tag.cs != L1D_CS_INVALID as u8 { - let phys_addr = l1d_tag_to_phys(tag, (idx << DCache::LINE_SHIFT) as u64); + let phys_addr = l1d_tag_to_phys(tag, (idx << Self::DC_LINE_SHIFT) as u64); self.invalidate_l2_line_phys(phys_addr); } @@ -1200,7 +1247,7 @@ impl R4000Cache { #[cfg(feature = "debug_cache")] if self.is_tracking_l2_idx(idx) { if l2_tag.cs() != L2_CS_INVALID { - let phys_base = l2_tag_to_phys(l2_tag, (idx << L2Cache::LINE_SHIFT) as u64); + let phys_base = l2_tag_to_phys(l2_tag, (idx << Self::L2_LINE_SHIFT) as u64); println!("[CACHE DEBUG] invalidate_l2_line: {} idx=0x{:x}, phys_base=0x{:08x}, ptag=0x{:05x}, cs={}", self.tracking_label_l2_idx(idx), idx, phys_base, l2_tag.ptag(), l2_tag.cs()); } else { @@ -1216,23 +1263,23 @@ impl R4000Cache { } // Reconstruct physical address range covered by this L2 line - let phys_base = l2_tag_to_phys(l2_tag, (idx << L2Cache::LINE_SHIFT) as u64); + let phys_base = l2_tag_to_phys(l2_tag, (idx << Self::L2_LINE_SHIFT) as u64); // NOTE: do NOT clear llbit here. On R4000, llbit tracks L1-D state only. // An L2 eviction is not a coherency action and must not break LL/SC. // R4K inclusive policy: cascade L2 eviction to L1. // R5K caches are non-inclusive — L2 evictions do not affect L1. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { // Check L1-I for any lines from this L2 line. // L1-I is VIPT so we must reconstruct the virtual index from pidx + physical sub-bits. let pidx = l2_tag.pidx(); - let l1i_lines_per_l2 = 1 << (L2Cache::LINE_SHIFT - ICache::LINE_SHIFT); + let l1i_lines_per_l2 = 1 << (Self::L2_LINE_SHIFT - Self::IC_LINE_SHIFT); let ic_base_idx = self.l2_idx_to_l1_base_idx(idx, pidx, &self.ic); for i in 0..l1i_lines_per_l2 { - let ic_idx = (ic_base_idx + i) & ICache::NUM_LINES_MASK; - let phys_addr = phys_base + ((i as u64) << ICache::LINE_SHIFT); + let ic_idx = (ic_base_idx + i) & Self::IC_NUM_LINES_MASK; + let phys_addr = phys_base + ((i as u64) << Self::IC_LINE_SHIFT); let ic_tag: L1ITag = self.ic.get_tag(ic_idx); if ic_tag.matches_phys(phys_addr) { self.invalidate_l1i_line(ic_idx, false); @@ -1241,11 +1288,11 @@ impl R4000Cache { // Check L1-D for any lines from this L2 line. // L1-D is VIPT so we must reconstruct the virtual index from pidx + physical sub-bits. - let l1d_lines_per_l2 = 1 << (L2Cache::LINE_SHIFT - DCache::LINE_SHIFT); + let l1d_lines_per_l2 = 1 << (Self::L2_LINE_SHIFT - Self::DC_LINE_SHIFT); let dc_base_idx = self.l2_idx_to_l1_base_idx(idx, pidx, &self.dc); for i in 0..l1d_lines_per_l2 { - let dc_idx = (dc_base_idx + i) & DCache::NUM_LINES_MASK; - let phys_addr = phys_base + ((i as u64) << DCache::LINE_SHIFT); + let dc_idx = (dc_base_idx + i) & Self::DC_NUM_LINES_MASK; + let phys_addr = phys_base + ((i as u64) << Self::DC_LINE_SHIFT); let dc_tag: L1DTag = self.dc.get_tag(dc_idx); if dc_tag.matches_phys(phys_addr) { @@ -1253,6 +1300,7 @@ impl R4000Cache { } } } + } // Finally invalidate the L2 line itself self.l2.set_tag(idx, L2Tag::default()); @@ -1283,7 +1331,7 @@ impl R4000Cache { /// and by the OS to ensure L2 coherency before DMA or cache mode changes. #[cfg(feature = "r5ksc_triton")] fn invall_l2(&self) { - for i in 0..L2Cache::NUM_LINES { + for i in 0..Self::L2_NUM_LINES { self.invalidate_l2_line(i); } } @@ -1314,7 +1362,7 @@ impl R4000Cache { } // Reconstruct physical address from tag - let phys_addr = l1d_tag_to_phys(tag, (l1_idx << DCache::LINE_SHIFT) as u64); + let phys_addr = l1d_tag_to_phys(tag, (l1_idx << Self::DC_LINE_SHIFT) as u64); #[cfg(feature = "debug_cache")] { @@ -1333,12 +1381,12 @@ impl R4000Cache { // R5K: non-inclusive L2 or no L2 — line may be absent or L2 disabled. // In all these cases write dirty data directly to memory. // R4K always holds the line in its inclusive L2, so this branch is r5k-only. - #[cfg(feature = "r5k")] + if Self::IS_R5K { if !self.l2_active() || l2_tag.cs() == L2_CS_INVALID || l2_tag.ptag() != l2_ptag { let dc_data = self.dc.data(); - let l1_start_chunk = l1_idx << DCache::CHUNKS_PER_LINE_SHIFT; - let line_base = phys_addr & !(DCache::LINE_MASK as u64); - let src = &dc_data[l1_start_chunk..l1_start_chunk + DCache::CHUNKS_PER_LINE]; + let l1_start_chunk = l1_idx << Self::DC_CHUNKS_PER_LINE_SHIFT; + let line_base = phys_addr & !(Self::DC_LINE_MASK as u64); + let src = &dc_data[l1_start_chunk..l1_start_chunk + Self::DC_CHUNKS_PER_LINE]; self.downstream.write_block(line_base as u32, src); let mut dc_tag: L1DTag = self.dc.get_tag(l1_idx); dc_tag.dirty = false; @@ -1346,23 +1394,25 @@ impl R4000Cache { self.dc.set_tag(l1_idx, dc_tag); return true; } + } // R4K (inclusive): L2 must always hold the line — fail if not. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { if l2_tag.ptag() != l2_ptag { return false; // shouldn't happen on inclusive R4K } + } // L2 has the line: write data from L1-D into L2. let dc_data = self.dc.data(); let l2_data = self.l2.data_mut(); - let l1_start_chunk = l1_idx << DCache::CHUNKS_PER_LINE_SHIFT; + let l1_start_chunk = l1_idx << Self::DC_CHUNKS_PER_LINE_SHIFT; - let l2_line_base = l2_idx << L2Cache::CHUNKS_PER_LINE_SHIFT; - let offset_in_l2_line = ((phys_addr & L2Cache::LINE_MASK as u64) >> 3) as usize; + let l2_line_base = l2_idx << Self::L2_CHUNKS_PER_LINE_SHIFT; + let offset_in_l2_line = ((phys_addr & Self::L2_LINE_MASK as u64) >> 3) as usize; - for i in 0..DCache::CHUNKS_PER_LINE { + for i in 0..Self::DC_CHUNKS_PER_LINE { l2_data[l2_line_base + offset_in_l2_line + i] = dc_data[l1_start_chunk + i]; } @@ -1370,8 +1420,8 @@ impl R4000Cache { { if self.is_tracking_l1d(phys_addr) || self.is_tracking_l2_idx(l2_idx) { println!("[CACHE DEBUG] writeback_l1d_line: wrote {} chunks to L2 idx=0x{:x} offset=0x{:x}", - DCache::CHUNKS_PER_LINE, l2_idx, offset_in_l2_line); - for i in 0..DCache::CHUNKS_PER_LINE { + Self::DC_CHUNKS_PER_LINE, l2_idx, offset_in_l2_line); + for i in 0..Self::DC_CHUNKS_PER_LINE { println!(" [{}] addr=0x{:08x} val=0x{:016x}", i, phys_addr + ((i as u64) << 3), dc_data[l1_start_chunk + i]); } @@ -1385,15 +1435,15 @@ impl R4000Cache { // not happen in fetch()'s hot path instead. r0/r1 (one chunk) are known // together, so r0's neighbor (r1) resolves immediately; r1's neighbor is // next iteration's r0, so it's finished one iteration late via `prev_s1`. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let l2_instrs = self.l2.instrs.get_mut(); - let instrs_start = (l2_idx << L2Cache::INSTR_SHIFT) + offset_in_l2_line * 2; + let instrs_start = (l2_idx << Self::L2_INSTR_SHIFT) + offset_in_l2_line * 2; #[cfg(feature = "opcodefusion")] - let dline_base = phys_addr & !(DCache::LINE_MASK as u64); + let dline_base = phys_addr & !(Self::DC_LINE_MASK as u64); #[cfg(feature = "opcodefusion")] let mut prev_s1: Option = None; - for i in 0..DCache::CHUNKS_PER_LINE { + for i in 0..Self::DC_CHUNKS_PER_LINE { let chunk = dc_data[l1_start_chunk + i]; let r0 = (chunk >> 32) as u32; let r1 = chunk as u32; @@ -1402,7 +1452,7 @@ impl R4000Cache { #[cfg(feature = "opcodefusion")] if let Some(prev_idx) = prev_s1.take() { let prev_phys = (dline_base as usize) + (i * 2 - 1) * 4; - if prev_phys & ICache::LINE_MASK != ICache::LINE_MASK - 3 { + if prev_phys & Self::IC_LINE_MASK != Self::IC_LINE_MASK - 3 { let prev = &mut l2_instrs[prev_idx]; prev.imm = r0; prev.flags |= FLAG_IMM_IS_NEXT; @@ -1414,7 +1464,7 @@ impl R4000Cache { #[cfg(feature = "opcodefusion")] { let word0_phys = (dline_base as usize) + (i * 2) * 4; - if word0_phys & ICache::LINE_MASK != ICache::LINE_MASK - 3 { + if word0_phys & Self::IC_LINE_MASK != Self::IC_LINE_MASK - 3 { s0.imm = r1; s0.flags |= FLAG_IMM_IS_NEXT; } @@ -1426,6 +1476,7 @@ impl R4000Cache { { prev_s1 = Some(idx1); } } } + } // Mark L2 line as dirty let new_cs = match l2_tag.cs() { @@ -1444,7 +1495,7 @@ impl R4000Cache { self.dc.set_tag(l1_idx, dc_tag); if cascade { - let phys_addr = l1d_tag_to_phys(tag, (l1_idx << DCache::LINE_SHIFT) as u64); + let phys_addr = l1d_tag_to_phys(tag, (l1_idx << Self::DC_LINE_SHIFT) as u64); self.writeback_l2_line_phys(phys_addr); } @@ -1458,7 +1509,7 @@ impl R4000Cache { let tag: L2Tag = self.l2.get_tag(idx); // Reconstruct physical address from tag - let phys_addr = l2_tag_to_phys(tag, (idx << L2Cache::LINE_SHIFT) as u64); + let phys_addr = l2_tag_to_phys(tag, (idx << Self::L2_LINE_SHIFT) as u64); // R4K: first flush dirty L1-D sub-lines into L2, so L2 has the authoritative data. // R5K: L1 and L2 are non-inclusive; dirty L1D lines hold the latest data and will @@ -1466,19 +1517,20 @@ impl R4000Cache { // dirty L1D line exists when we write back L2 to memory we may lose data, but the // CACHE-op sequence IRIX uses (C_IWBINV L1D then C_IWBINV L2) ensures L1D is clean // before L2 is touched. For safety we scan and flush anyway on R5K too. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { - let l1d_lines_per_l2 = 1 << (L2Cache::LINE_SHIFT - DCache::LINE_SHIFT); + let l1d_lines_per_l2 = 1 << (Self::L2_LINE_SHIFT - Self::DC_LINE_SHIFT); let dc_base_idx = self.l2_idx_to_l1_base_idx(idx, tag.pidx(), &self.dc); for i in 0..l1d_lines_per_l2 { - let dc_idx = (dc_base_idx + i) & DCache::NUM_LINES_MASK; - let phys_addr_l1 = phys_addr + ((i as u64) << DCache::LINE_SHIFT); + let dc_idx = (dc_base_idx + i) & Self::DC_NUM_LINES_MASK; + let phys_addr_l1 = phys_addr + ((i as u64) << Self::DC_LINE_SHIFT); let dc_tag: L1DTag = self.dc.get_tag(dc_idx); if dc_tag.matches_phys(phys_addr_l1) { self.writeback_l1d_line(dc_idx, false); } } } + } // Now check if L2 line is dirty (may have become dirty from L1-D writeback) let mut tag: L2Tag = self.l2.get_tag(idx); @@ -1493,9 +1545,9 @@ impl R4000Cache { self.tracking_label_l2_idx(idx), idx, phys_addr, tag.ptag(), cs); // Dump the L2 line data being written let l2_data = self.l2.data(); - let start_chunk = idx << L2Cache::CHUNKS_PER_LINE_SHIFT; + let start_chunk = idx << Self::L2_CHUNKS_PER_LINE_SHIFT; println!(" L2 line data being written (16 x u64):"); - for i in 0..L2Cache::CHUNKS_PER_LINE { + for i in 0..Self::L2_CHUNKS_PER_LINE { let val = l2_data[start_chunk + i]; println!(" [{}] addr=0x{:08x} val=0x{:016x}", i, phys_addr + ((i as u64) << 3), val); } @@ -1506,8 +1558,8 @@ impl R4000Cache { // Now write L2 data to memory let l2_data = self.l2.data(); - let start_chunk = idx << L2Cache::CHUNKS_PER_LINE_SHIFT; - let src = &l2_data[start_chunk..start_chunk + L2Cache::CHUNKS_PER_LINE]; + let start_chunk = idx << Self::L2_CHUNKS_PER_LINE_SHIFT; + let src = &l2_data[start_chunk..start_chunk + Self::L2_CHUNKS_PER_LINE]; if self.downstream.write_block(phys_addr as u32, src) != BUS_OK { return false; } @@ -1531,18 +1583,17 @@ impl R4000Cache { self.invalidate_l2_line(l2_idx); // Calculate line-aligned address - let line_base = phys_addr & !(L2Cache::LINE_MASK as u64); + let line_base = phys_addr & !(Self::L2_LINE_MASK as u64); // Fill line from memory let l2_data = self.l2.data_mut(); - let start_chunk = l2_idx << L2Cache::CHUNKS_PER_LINE_SHIFT; + let start_chunk = l2_idx << Self::L2_CHUNKS_PER_LINE_SHIFT; // INVARIANT: l2.data is always accessed as u64 chunks (never as u32 words). - // R4K: l2.instrs[n] mirrors the n-th instruction word; fetch() indexes it directly. - // R5K: l2.instrs is empty — fill_l1i_line reads raw words from l2.data instead. + // Direct-mapped: l2.instrs[n] mirrors word n and fetch() indexes it directly. + // 2-way: l2.instrs is empty — fill_l1i_line reads raw words from l2.data. // Do not add data_as_words() accessors on L2 or fetch indexing will silently break. - #[cfg(not(feature = "r5k"))] - let instrs_start = l2_idx << L2Cache::INSTR_SHIFT; + let instrs_start = l2_idx << Self::L2_INSTR_SHIFT; // Delay-slot fusion lookahead (FLAG_IMM_IS_NEXT) is computed inline below, // where raw values are already hot, instead of in fetch()'s hot path — // fetch() must stay a plain shared-borrow read for the common case (cache @@ -1555,19 +1606,19 @@ impl R4000Cache { // late via `prev_s1`. An L2 line always contains a whole number of L1I // lines, so the neighbor is only out-of-bounds at each L1I sub-line's // last word (checked via physical address, via `prev_s1`/`fuse_pair!`). - #[cfg(all(not(feature = "r5k"), feature = "opcodefusion"))] + #[cfg(feature = "opcodefusion")] macro_rules! fuse_pair { ($l2_instrs:expr, $prev_s1:expr, $i:expr, $idx0:expr, $idx1:expr, $r0:expr, $r1:expr) => { if let Some(prev_idx) = $prev_s1.take() { let prev_phys = (line_base as usize) + ($i * 2 - 1) * 4; - if prev_phys & ICache::LINE_MASK != ICache::LINE_MASK - 3 { + if prev_phys & Self::IC_LINE_MASK != Self::IC_LINE_MASK - 3 { let prev = &mut $l2_instrs[prev_idx]; prev.imm = $r0; prev.flags |= FLAG_IMM_IS_NEXT; } } let word0_phys = (line_base as usize) + ($i * 2) * 4; - if word0_phys & ICache::LINE_MASK != ICache::LINE_MASK - 3 { + if word0_phys & Self::IC_LINE_MASK != Self::IC_LINE_MASK - 3 { let s0 = &mut $l2_instrs[$idx0]; s0.imm = $r1; s0.flags |= FLAG_IMM_IS_NEXT; @@ -1575,22 +1626,20 @@ impl R4000Cache { $prev_s1 = Some($idx1); }; } - #[cfg(all(not(feature = "r5k"), not(feature = "opcodefusion")))] + #[cfg(not(feature = "opcodefusion"))] macro_rules! fuse_pair { ($l2_instrs:expr, $prev_s1:expr, $i:expr, $idx0:expr, $idx1:expr, $r0:expr, $r1:expr) => {}; } if let Some(src) = self.downstream.mem_ptr(line_base as u32) { // Fast path: single pass over source — rotate into l2.data and fill l2.instrs. - #[cfg(not(feature = "r5k"))] let l2_instrs = self.l2.instrs.get_mut(); // Only fuse_pair! mutates prev_s1, and it is a no-op without opcodefusion. - #[cfg(not(feature = "r5k"))] #[cfg_attr(not(feature = "opcodefusion"), allow(unused_mut))] let mut prev_s1: Option = None; - for i in 0..L2Cache::CHUNKS_PER_LINE { + for i in 0..Self::L2_CHUNKS_PER_LINE { let val = unsafe { (*src.add(i)).rotate_left(32) }; l2_data[start_chunk + i] = val; - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let r0 = (val >> 32) as u32; let r1 = val as u32; @@ -1604,17 +1653,18 @@ impl R4000Cache { s1.raw = r1; fuse_pair!(l2_instrs, prev_s1, i, idx0, idx1, r0, r1); } + } } } else { - let dest = &mut l2_data[start_chunk..start_chunk + L2Cache::CHUNKS_PER_LINE]; + let dest = &mut l2_data[start_chunk..start_chunk + Self::L2_CHUNKS_PER_LINE]; let s = self.downstream.read_block(line_base as u32, dest); if s != crate::traits::BUS_OK { return false; } - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let l2_instrs = self.l2.instrs.get_mut(); #[cfg_attr(not(feature = "opcodefusion"), allow(unused_mut))] let mut prev_s1: Option = None; - for i in 0..L2Cache::CHUNKS_PER_LINE { + for i in 0..Self::L2_CHUNKS_PER_LINE { let val = dest[i]; let r0 = (val >> 32) as u32; let r1 = val as u32; @@ -1629,6 +1679,7 @@ impl R4000Cache { fuse_pair!(l2_instrs, prev_s1, i, idx0, idx1, r0, r1); } } + } } // Set tag with CleanExclusive state @@ -1648,7 +1699,7 @@ impl R4000Cache { println!("[CACHE DEBUG] fill_l2_line: {} line 0x{:08x}, idx=0x{:x}, phys_addr=0x{:08x}, ptag=0x{:05x}, pidx={}", self.tracking_label_l2_idx(l2_idx), line_base, l2_idx, phys_addr, ptag, pidx); println!(" L2 line data (16 x u64):"); - for i in 0..L2Cache::CHUNKS_PER_LINE { + for i in 0..Self::L2_CHUNKS_PER_LINE { let val = l2_data[start_chunk + i]; println!(" [{}] 0x{:016x}", i, val); } @@ -1664,11 +1715,10 @@ impl R4000Cache { fn fill_l1i_line(&self, index_addr: u64, phys_addr: u64) -> u32 { let set = self.ic.get_index(index_addr); - // R5K: 2-way — pick LRU way; R4K: single way, eidx == set. - #[cfg(not(feature = "r5k"))] - let ic_eidx = set; - #[cfg(feature = "r5k")] - let ic_eidx = set | (unsafe { Self::lru_get(self.ic_lru.get(), set) } as usize) << ICache::NUM_LINES_SHIFT; + // 2-way picks the LRU way; direct-mapped has one way, so eidx == set. + let ic_eidx = if Self::IS_R5K { + set | (unsafe { Self::lru_get(self.ic_lru.get(), set) } as usize) << Self::IC_NUM_LINES_SHIFT + } else { set }; // Invalidate victim slot unconditionally — clears tag before any early return. self.invalidate_l1i_line(ic_eidx, false); @@ -1683,10 +1733,11 @@ impl R4000Cache { if l2_hit { // R4K only: check for Virtual Coherency Exception (VCEI). // R5K dropped VCE — no pidx tracking needed. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { if self.pidx(index_addr) != l2_tag.pidx() { return exec_exception_const(EXC_VCEI); } + } } else { // L2 miss — fill from memory into L2 first. #[cfg(not(feature = "lightning"))] @@ -1704,19 +1755,17 @@ impl R4000Cache { crate::dlog!(LogModule::L1i, "fill virt={:#x} phys={:#x} eidx={}", index_addr, phys_addr, ic_eidx); } - // R5K: populate ic_instrs for this way's slot. - // Source is l2.data when L2 is active, or memory directly when L2 is disabled. - #[cfg(feature = "r5k")] - { - let ic_slot_base = ic_eidx << ICache::INSTR_SHIFT; + // 2-way only: populate this way's ic_instrs slot from L2, or memory if L2 is off. + if Self::IS_R5K { + let ic_slot_base = ic_eidx << Self::IC_INSTR_SHIFT; let ic_instrs = self.ic_instrs.get_mut(); if self.l2_active() { - let l2_sub_offset = ((phys_addr as usize) & (L2Cache::LINE_MASK & !ICache::LINE_MASK)) >> 3; - let l2_chunk_base = (self.l2.get_index(phys_addr) << L2Cache::CHUNKS_PER_LINE_SHIFT) + let l2_sub_offset = ((phys_addr as usize) & (Self::L2_LINE_MASK & !Self::IC_LINE_MASK)) >> 3; + let l2_chunk_base = (self.l2.get_index(phys_addr) << Self::L2_CHUNKS_PER_LINE_SHIFT) + l2_sub_offset; let l2_data = self.l2.data(); let src = unsafe { l2_data.as_ptr().add(l2_chunk_base) }; - for i in 0..ICache::INSTRS_PER_LINE / 2 { + for i in 0..Self::IC_INSTRS_PER_LINE / 2 { let chunk = unsafe { *src.add(i) }; let w0 = (chunk >> 32) as u32; let w1 = chunk as u32; @@ -1729,13 +1778,13 @@ impl R4000Cache { } } else { // L2 disabled: read directly from memory. - let line_base = phys_addr & !(ICache::LINE_MASK as u64); + let line_base = phys_addr & !(Self::IC_LINE_MASK as u64); if let Some(src) = self.downstream.mem_ptr(line_base as u32) { // Fast path: read word pairs directly from backing store. // mem_ptr's raw u64s are host-native pairs of little-endian-loaded // words; rotate_left(32) puts them in MIPS big-endian word order // (high word = first instr), matching read_block's behavior. - for i in 0..ICache::INSTRS_PER_LINE / 2 { + for i in 0..Self::IC_INSTRS_PER_LINE / 2 { let chunk = unsafe { (*src.add(i)).rotate_left(32) }; let w0 = (chunk >> 32) as u32; let w1 = chunk as u32; @@ -1747,7 +1796,7 @@ impl R4000Cache { d1.raw = w1; } } else { - for i in 0..ICache::INSTRS_PER_LINE { + for i in 0..Self::IC_INSTRS_PER_LINE { let word_addr = (line_base + (i as u64) * 4) as u32; let r = self.downstream.read32(word_addr); let w = if r.is_ok() { r.data } else { 0 }; @@ -1758,7 +1807,7 @@ impl R4000Cache { } } // Flip LRU: just-filled way becomes MRU. - let way = ic_eidx >> ICache::NUM_LINES_SHIFT; + let way = ic_eidx >> Self::IC_NUM_LINES_SHIFT; unsafe { Self::lru_set(self.ic_lru.get(), set, way ^ 1); } } @@ -1766,16 +1815,15 @@ impl R4000Cache { #[cfg(feature = "debug_cache")] if self.is_tracking_addr(index_addr, phys_addr) || self.is_tracking_l2_idx(self.l2.get_index(phys_addr)) { - let way = ic_eidx >> ICache::NUM_LINES_SHIFT; - let set = ic_eidx & ICache::NUM_LINES_MASK; + let way = ic_eidx >> Self::IC_NUM_LINES_SHIFT; + let set = ic_eidx & Self::IC_NUM_LINES_MASK; println!("[CACHE DEBUG] fill_l1i_line: {} virt 0x{:016x} phys 0x{:016x} → L1I eidx=0x{:x} way={} set=0x{:x}", self.tracking_label(phys_addr), index_addr, phys_addr, ic_eidx, way, set); - #[cfg(feature = "r5k")] - { + if Self::IS_R5K { let ic_instrs = self.ic_instrs.get(); - let slot_base = ic_eidx << ICache::INSTR_SHIFT; + let slot_base = ic_eidx << Self::IC_INSTR_SHIFT; print!(" ic_instrs:"); - for i in 0..ICache::INSTRS_PER_LINE { + for i in 0..Self::IC_INSTRS_PER_LINE { if i % 4 == 0 { print!("\n "); } print!("{:08x} ", ic_instrs[slot_base + i].raw); } @@ -1783,7 +1831,7 @@ impl R4000Cache { } } - (ic_eidx >> ICache::NUM_LINES_SHIFT) as u32 + (ic_eidx >> Self::IC_NUM_LINES_SHIFT) as u32 } /// Fill L1 data cache line. Ensures data is in L2 first, then copies to L1-D. @@ -1794,15 +1842,12 @@ impl R4000Cache { /// >1 = BUS_VCE or BUS_ERR fn fill_l1d_line(&self, virt_addr: u64, phys_addr: u64) -> u32 { // For R5K: pick victim way via LRU and encode into dc_idx via shift. - // dc_ext_idx = set | (way << DCache::NUM_LINES_SHIFT) - #[cfg(not(feature = "r5k"))] - let (victim_way, dc_idx) = (0usize, self.dc.get_index(virt_addr)); - #[cfg(feature = "r5k")] - let (victim_way, dc_idx) = { + // dc_ext_idx = set | (way << Self::DC_NUM_LINES_SHIFT) + let (victim_way, dc_idx) = if Self::IS_R5K { let set = self.dc.get_index(virt_addr); let way = unsafe { Self::lru_get(self.dc_lru.get(), set) } as usize; - (way, set | (way << DCache::NUM_LINES_SHIFT)) - }; + (way, set | (way << Self::DC_NUM_LINES_SHIFT)) + } else { (0usize, self.dc.get_index(virt_addr)) }; // Writeback and invalidate the victim line (hardware fill — not a coherency action) self.writeback_l1d_line(dc_idx, false); @@ -1815,8 +1860,9 @@ impl R4000Cache { let l2_ptag = self.l2_ptag(phys_addr); if l2_tag.cs() != L2_CS_INVALID && l2_tag.ptag() == l2_ptag { // Check for Virtual Coherency Exception (R4K only; R5K dropped VCE) - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { if self.pidx(virt_addr) != l2_tag.pidx() { return BUS_VCE; } + } true } else { #[cfg(not(feature = "lightning"))] @@ -1835,42 +1881,43 @@ impl R4000Cache { } let dc_data = self.dc.data_mut(); - let dc_start_chunk = dc_idx << DCache::CHUNKS_PER_LINE_SHIFT; + let dc_start_chunk = dc_idx << Self::DC_CHUNKS_PER_LINE_SHIFT; if l2_hit { // Copy from L2 to L1-D - let dc_line_base = phys_addr & !(DCache::LINE_MASK as u64); + let dc_line_base = phys_addr & !(Self::DC_LINE_MASK as u64); let l2_idx = self.l2.get_index(phys_addr); - let l2_line_base = l2_idx << L2Cache::CHUNKS_PER_LINE_SHIFT; - let offset_in_l2_line = ((dc_line_base & (L2Cache::LINE_MASK as u64)) >> 3) as usize; + let l2_line_base = l2_idx << Self::L2_CHUNKS_PER_LINE_SHIFT; + let offset_in_l2_line = ((dc_line_base & (Self::L2_LINE_MASK as u64)) >> 3) as usize; let l2_data = self.l2.data(); - for i in 0..DCache::CHUNKS_PER_LINE { + for i in 0..Self::DC_CHUNKS_PER_LINE { dc_data[dc_start_chunk + i] = l2_data[l2_line_base + offset_in_l2_line + i]; } } else { // L2 disabled: copy directly from memory - let line_base = phys_addr & !(DCache::LINE_MASK as u64); - let dest = &mut dc_data[dc_start_chunk..dc_start_chunk + DCache::CHUNKS_PER_LINE]; + let line_base = phys_addr & !(Self::DC_LINE_MASK as u64); + let dest = &mut dc_data[dc_start_chunk..dc_start_chunk + Self::DC_CHUNKS_PER_LINE]; self.downstream.read_block(line_base as u32, dest); } self.dc.set_tag(dc_idx, L1DTag::valid(phys_addr, L1D_CS_CLEAN_EXCLUSIVE as u8, false)); // R5K: flip LRU — filled way is MRU, other way is now victim - #[cfg(feature = "r5k")] - unsafe { Self::lru_set(self.dc_lru.get(), dc_idx & DCache::NUM_LINES_MASK, victim_way ^ 1); } + if Self::IS_R5K { + unsafe { Self::lru_set(self.dc_lru.get(), dc_idx & Self::DC_NUM_LINES_MASK, victim_way ^ 1); } + } #[cfg(feature = "debug_cache")] { - let line_base_phys = phys_addr & !(DCache::LINE_MASK as u64); + let line_base_phys = phys_addr & !(Self::DC_LINE_MASK as u64); let l2_idx_check = self.l2.get_index(phys_addr); if self.is_tracking_l1d(line_base_phys) || self.is_tracking_l2_idx(l2_idx_check) { - let line_base_virt = virt_addr & !(DCache::LINE_MASK as u64); - let way = dc_idx >> DCache::NUM_LINES_SHIFT; - let set = dc_idx & DCache::NUM_LINES_MASK; + let line_base_virt = virt_addr & !(Self::DC_LINE_MASK as u64); + let way = dc_idx >> Self::DC_NUM_LINES_SHIFT; + let set = dc_idx & Self::DC_NUM_LINES_MASK; println!("[CACHE DEBUG] fill_l1d_line: {} virt 0x{:016x} phys 0x{:016x} → L1D eidx=0x{:x} way={} set=0x{:x}", self.tracking_label(line_base_phys), line_base_virt, line_base_phys, dc_idx, way, set); - for i in 0..DCache::CHUNKS_PER_LINE { + for i in 0..Self::DC_CHUNKS_PER_LINE { println!(" [{}] 0x{:016x}", i, dc_data[dc_start_chunk + i]); } } @@ -1887,17 +1934,10 @@ impl R4000Cache { /// >1 = BUS_VCE or BUS_ERR — propagate as error status /// /// Callers check `way <= 1` for success. - /// `dc_ext_idx` for tag/data access = `set | (way << DCache::NUM_LINES_SHIFT)`. + /// `dc_ext_idx` for tag/data access = `set | (way << Self::DC_NUM_LINES_SHIFT)`. #[inline(always)] fn ensure_l1d_line(&self, virt_addr: u64, phys_addr: u64) -> u32 { - #[cfg(not(feature = "r5k"))] - { - let dc_idx = self.dc.get_index(virt_addr); - let dc_tag: L1DTag = self.dc.get_tag(dc_idx); - if dc_tag.matches_phys(phys_addr) { 0 } - else { self.fill_l1d_line(virt_addr, phys_addr) } - } - #[cfg(feature = "r5k")] + if Self::IS_R5K { { let set = self.dc.get_index(virt_addr); if self.dc.get_tag(set).matches_phys(phys_addr) { @@ -1905,30 +1945,38 @@ impl R4000Cache { unsafe { Self::lru_set(self.dc_lru.get(), set, 1); } return 0; } - if self.dc.get_tag(set | (1 << DCache::NUM_LINES_SHIFT)).matches_phys(phys_addr) { + if self.dc.get_tag(set | (1 << Self::DC_NUM_LINES_SHIFT)).matches_phys(phys_addr) { // way1 hit → way1 is MRU, way0 is LRU next unsafe { Self::lru_set(self.dc_lru.get(), set, 0); } return 1; } self.fill_l1d_line(virt_addr, phys_addr) } + } else { + { + let dc_idx = self.dc.get_index(virt_addr); + let dc_tag: L1DTag = self.dc.get_tag(dc_idx); + if dc_tag.matches_phys(phys_addr) { 0 } + else { self.fill_l1d_line(virt_addr, phys_addr) } + } + } } /// Compute the data-array address for `dc.dc_read/dc_write` from the extended tag index /// and the original virtual address. - /// dc_ext_idx = set | (way << DCache::NUM_LINES_SHIFT) + /// dc_ext_idx = set | (way << Self::DC_NUM_LINES_SHIFT) /// → data address = (dc_ext_idx << LINE_SHIFT) | (virt_addr & LINE_MASK) /// Way1 data lives in [DC_SIZE/2, DC_SIZE); dc_read/dc_write mask to (DC_SIZE-1) so this /// routes both ways into the correct half of the allocated data array. /// For R4K (1-way), dc_ext_idx = dc_idx so this equals the original virt_addr. #[inline(always)] fn dc_data_addr(dc_ext_idx: usize, virt_addr: u64) -> u64 { - ((dc_ext_idx << DCache::LINE_SHIFT as usize) as u64) - | (virt_addr & DCache::LINE_MASK as u64) + ((dc_ext_idx << Self::DC_LINE_SHIFT as usize) as u64) + | (virt_addr & Self::DC_LINE_MASK as u64) } /// Mark the L1-D line as dirty. - /// `dc_ext_idx` = `set | (way << DCache::NUM_LINES_SHIFT)` (returned by ensure_l1d_line). + /// `dc_ext_idx` = `set | (way << Self::DC_NUM_LINES_SHIFT)` (returned by ensure_l1d_line). /// For R4K it is just `get_index(virt_addr)`. #[inline(always)] fn mark_l1d_dirty(&self, dc_ext_idx: usize) { @@ -1943,7 +1991,7 @@ impl R4000Cache { fn hit_l1i(&self, virt_addr: u64, phys_addr: u64) -> Option { let set = self.ic.get_index(virt_addr); for way in 0..IC_WAYS { - let eidx = set | (way << ICache::NUM_LINES_SHIFT); + let eidx = set | (way << Self::IC_NUM_LINES_SHIFT); let tag: L1ITag = self.ic.get_tag(eidx); if tag.matches_phys(phys_addr) { return Some(eidx); } } @@ -1955,7 +2003,7 @@ impl R4000Cache { fn hit_l1d(&self, virt_addr: u64, phys_addr: u64) -> Option { let set = self.dc.get_index(virt_addr); for way in 0..DC_WAYS { - let eidx = set | (way << DCache::NUM_LINES_SHIFT); + let eidx = set | (way << Self::DC_NUM_LINES_SHIFT); let tag: L1DTag = self.dc.get_tag(eidx); if tag.matches_phys(phys_addr) { return Some(eidx); } } @@ -1977,102 +2025,125 @@ impl R4000Cache { } } -impl MipsCache for R4000Cache { - #[cfg(not(feature = "r5k"))] +impl CpuModel for CpuCache { + const MIPS4: bool = MIPS4; + const PRID: u32 = PRID; + const FIR: u32 = FIR; + const TLB_ENTRIES: usize = TLB_ENTRIES; + const NAME: &'static str = if IC_WAYS == 2 { "R5000" } else { "R4400" }; +} + +impl MipsCache for CpuCache { + const IC_SIZE: usize = IC_SIZE; + const IC_LINE: usize = IC_LINE; + const IC_WAYS: usize = IC_WAYS; + const DC_SIZE: usize = DC_SIZE; + const DC_LINE: usize = DC_LINE; + const DC_WAYS: usize = DC_WAYS; + const L2_SIZE: usize = if HAS_L2 { L2_CACHE_SIZE } else { 0 }; + const L2_LINE: usize = L2_LINE; + fn fetch(&self, virt_addr: u64, phys_addr: u64) -> FetchInstrResult { - #[cfg(feature = "debug_cache")] - let tracked = { - if self.is_tracking_addr(virt_addr, phys_addr) { - println!("[CACHE DEBUG] fetch: {} virt_addr 0x{:016x}, phys_addr 0x{:016x}", - self.tracking_label(phys_addr), virt_addr, phys_addr); - true - } else { - let l2_idx = self.l2.get_index(phys_addr); - if self.is_tracking_l2_idx(l2_idx) { - let line_base = phys_addr & !(L2Cache::LINE_MASK as u64); - println!("[CACHE DEBUG] fetch (L2 alias): idx=0x{:x}, line 0x{:08x}, virt 0x{:016x}, phys 0x{:016x}", - l2_idx, line_base, virt_addr, phys_addr); - true - } else { - false + if Self::IS_R5K { + #[cfg(feature = "debug_cache")] + { + if self.is_tracking_addr(virt_addr, phys_addr) { + println!("[CACHE DEBUG] fetch: {} virt_addr 0x{:016x}, phys_addr 0x{:016x}", + self.tracking_label(phys_addr), virt_addr, phys_addr); } } - }; - let ic_idx = self.ic.get_index(virt_addr); - let ic_tag: L1ITag = self.ic.get_tag(ic_idx); + let set = self.ic.get_index(virt_addr); + let way1_base = 1 << Self::IC_NUM_LINES_SHIFT; - #[cfg(feature = "developer")] - self.l1i_fetch_count.fetch_add(1, Ordering::Relaxed); - if !ic_tag.matches_phys(phys_addr) { - let s = self.fill_l1i_line(virt_addr, phys_addr); - if s != 0 { return FetchInstrResult::exception(s); } - } else { #[cfg(feature = "developer")] - self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); - } + self.l1i_fetch_count.fetch_add(1, Ordering::Relaxed); - { - // Plain shared-borrow read, matching the pre-fusion hot path exactly. - // FLAG_IMM_IS_NEXT is precomputed at fill time (see fill_l2_line), not - // here — see the comment there for why fetch() must not take get_mut(). - let l2_slot_idx = ((phys_addr as usize) & (L2_CACHE_SIZE - 1)) >> 2; - let slot = &self.l2.instrs.get()[l2_slot_idx] as *const DecodedInstr; - #[cfg(feature = "debug_cache")] - if tracked { - let raw = unsafe { (*slot).raw }; - println!("[CACHE DEBUG] fetch: virt 0x{:016x}, phys 0x{:016x} -> raw=0x{:08x}", - virt_addr, phys_addr, raw); - } - FetchInstrResult::hit(slot) - } - } + let ic_eidx = if self.ic.get_tag(set).matches_phys(phys_addr) { + #[cfg(feature = "developer")] + self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); + #[cfg(not(feature = "lightning"))] + if devlog_is_active(LogModule::L1i) && devlog_mask(LogModule::L1i) & CACHE_LOG_HIT != 0 { + crate::dlog!(LogModule::L1i, "hit virt={:#x} phys={:#x} set={} way=0", virt_addr, phys_addr, set); + } + unsafe { Self::lru_set(self.ic_lru.get(), set, 1); } // way0 MRU → way1 is LRU + set + } else if self.ic.get_tag(set | way1_base).matches_phys(phys_addr) { + #[cfg(feature = "developer")] + self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); + #[cfg(not(feature = "lightning"))] + if devlog_is_active(LogModule::L1i) && devlog_mask(LogModule::L1i) & CACHE_LOG_HIT != 0 { + crate::dlog!(LogModule::L1i, "hit virt={:#x} phys={:#x} set={} way=1", virt_addr, phys_addr, set); + } + unsafe { Self::lru_set(self.ic_lru.get(), set, 0); } // way1 MRU → way0 is LRU + set | way1_base + } else { + let way = self.fill_l1i_line(virt_addr, phys_addr); + if way > 1 { return FetchInstrResult::exception(way); } + set | (way as usize) << Self::IC_NUM_LINES_SHIFT + }; - #[cfg(feature = "r5k")] - fn fetch(&self, virt_addr: u64, phys_addr: u64) -> FetchInstrResult { - #[cfg(feature = "debug_cache")] - { - if self.is_tracking_addr(virt_addr, phys_addr) { - println!("[CACHE DEBUG] fetch: {} virt_addr 0x{:016x}, phys_addr 0x{:016x}", - self.tracking_label(phys_addr), virt_addr, phys_addr); + let instr_idx = (ic_eidx << Self::IC_INSTR_SHIFT) + | ((virt_addr as usize >> 2) & Self::IC_INSTR_MASK); + { + let slot = &self.ic_instrs.get()[instr_idx] as *const DecodedInstr; + FetchInstrResult::hit(slot) } - } - - let set = self.ic.get_index(virt_addr); - let way1_base = 1 << ICache::NUM_LINES_SHIFT; + } else { + #[cfg(feature = "debug_cache")] + let tracked = { + if self.is_tracking_addr(virt_addr, phys_addr) { + println!("[CACHE DEBUG] fetch: {} virt_addr 0x{:016x}, phys_addr 0x{:016x}", + self.tracking_label(phys_addr), virt_addr, phys_addr); + true + } else { + let l2_idx = self.l2.get_index(phys_addr); + if self.is_tracking_l2_idx(l2_idx) { + let line_base = phys_addr & !(Self::L2_LINE_MASK as u64); + println!("[CACHE DEBUG] fetch (L2 alias): idx=0x{:x}, line 0x{:08x}, virt 0x{:016x}, phys 0x{:016x}", + l2_idx, line_base, virt_addr, phys_addr); + true + } else { + false + } + } + }; - #[cfg(feature = "developer")] - self.l1i_fetch_count.fetch_add(1, Ordering::Relaxed); + let ic_idx = self.ic.get_index(virt_addr); + let ic_tag: L1ITag = self.ic.get_tag(ic_idx); - let ic_eidx = if self.ic.get_tag(set).matches_phys(phys_addr) { #[cfg(feature = "developer")] - self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); - #[cfg(not(feature = "lightning"))] - if devlog_is_active(LogModule::L1i) && devlog_mask(LogModule::L1i) & CACHE_LOG_HIT != 0 { - crate::dlog!(LogModule::L1i, "hit virt={:#x} phys={:#x} set={} way=0", virt_addr, phys_addr, set); - } - unsafe { Self::lru_set(self.ic_lru.get(), set, 1); } // way0 MRU → way1 is LRU - set - } else if self.ic.get_tag(set | way1_base).matches_phys(phys_addr) { - #[cfg(feature = "developer")] - self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); - #[cfg(not(feature = "lightning"))] - if devlog_is_active(LogModule::L1i) && devlog_mask(LogModule::L1i) & CACHE_LOG_HIT != 0 { - crate::dlog!(LogModule::L1i, "hit virt={:#x} phys={:#x} set={} way=1", virt_addr, phys_addr, set); + self.l1i_fetch_count.fetch_add(1, Ordering::Relaxed); + if !ic_tag.matches_phys(phys_addr) { + let s = self.fill_l1i_line(virt_addr, phys_addr); + if s != 0 { return FetchInstrResult::exception(s); } + } else { + #[cfg(feature = "developer")] + self.l1i_hit_count.fetch_add(1, Ordering::Relaxed); } - unsafe { Self::lru_set(self.ic_lru.get(), set, 0); } // way1 MRU → way0 is LRU - set | way1_base - } else { - let way = self.fill_l1i_line(virt_addr, phys_addr); - if way > 1 { return FetchInstrResult::exception(way); } - set | (way as usize) << ICache::NUM_LINES_SHIFT - }; - let instr_idx = (ic_eidx << ICache::INSTR_SHIFT) - | ((virt_addr as usize >> 2) & ICache::INSTR_MASK); - { - let slot = &self.ic_instrs.get()[instr_idx] as *const DecodedInstr; - FetchInstrResult::hit(slot) + { + // Plain shared-borrow read, matching the pre-fusion hot path exactly. + // FLAG_IMM_IS_NEXT is precomputed at fill time (see fill_l2_line), not + // here — see the comment there for why fetch() must not take get_mut(). + let l2_slot_idx = ((phys_addr as usize) & (L2_CACHE_SIZE - 1)) >> 2; + let slot = &self.l2.instrs.get()[l2_slot_idx] as *const DecodedInstr; + #[cfg(feature = "debug_cache")] + if tracked { + let raw = unsafe { (*slot).raw }; + println!("[CACHE DEBUG] fetch: virt 0x{:016x}, phys 0x{:016x} -> raw=0x{:08x}", + virt_addr, phys_addr, raw); + } + FetchInstrResult::hit(slot) + } } } @@ -2087,7 +2158,7 @@ impl MipsCache for R4000Cache { // Also track reads that will hit the same L2 index (cache line aliasing) let l2_idx = self.l2.get_index(phys_addr); if self.is_tracking_l2_idx(l2_idx) { - let line_base = phys_addr & !(L2Cache::LINE_MASK as u64); + let line_base = phys_addr & !(Self::L2_LINE_MASK as u64); println!("[CACHE DEBUG] read (L2 alias): idx=0x{:x}, line 0x{:08x}, virt 0x{:016x}, phys 0x{:016x}, size {}", l2_idx, line_base, virt_addr, phys_addr, SIZE); } @@ -2096,7 +2167,7 @@ impl MipsCache for R4000Cache { // R4K (1-way): way is always 0, dc_eidx == get_index(virt_addr), da == virt_addr. // Skip the generic ensure_l1d_line/dc_data_addr indirection entirely. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let dc_idx = self.dc.get_index(virt_addr); if !self.dc.get_tag(dc_idx).matches_phys(phys_addr) { @@ -2115,12 +2186,12 @@ impl MipsCache for R4000Cache { } return BusRead64::ok(result); } + } // R5K (2-way): use generic path with way encoding. - #[cfg(feature = "r5k")] { let way = self.ensure_l1d_line(virt_addr, phys_addr); if way > 1 { return BusRead64 { status: way, data: 0 }; } - let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << DCache::NUM_LINES_SHIFT; + let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << Self::DC_NUM_LINES_SHIFT; #[cfg(not(feature = "lightning"))] if devlog_is_active(LogModule::L1d) && devlog_mask(LogModule::L1d) & CACHE_LOG_HIT != 0 { crate::dlog!(LogModule::L1d, "read{} hit virt={:#x} phys={:#x} eidx={}", SIZE, virt_addr, phys_addr, dc_eidx); @@ -2147,7 +2218,7 @@ impl MipsCache for R4000Cache { } // R4K (1-way): way always 0, dc_eidx == get_index(virt_addr), da == virt_addr. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let dc_idx = self.dc.get_index(virt_addr); if !self.dc.get_tag(dc_idx).matches_phys(phys_addr) { @@ -2162,12 +2233,12 @@ impl MipsCache for R4000Cache { self.mark_l1d_dirty(dc_idx); return BUS_OK; } + } // R5K (2-way): generic path. - #[cfg(feature = "r5k")] { let way = self.ensure_l1d_line(virt_addr, phys_addr); if way > 1 { return way; } - let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << DCache::NUM_LINES_SHIFT; + let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << Self::DC_NUM_LINES_SHIFT; #[cfg(not(feature = "lightning"))] if devlog_is_active(LogModule::L1d) && devlog_mask(LogModule::L1d) & CACHE_LOG_HIT != 0 { crate::dlog!(LogModule::L1d, "write{} hit virt={:#x} phys={:#x} eidx={} val={:#x}", SIZE, virt_addr, phys_addr, dc_eidx, val); @@ -2190,7 +2261,7 @@ impl MipsCache for R4000Cache { } // R4K (1-way): da == virt_addr directly. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let dc_idx = self.dc.get_index(virt_addr); if !self.dc.get_tag(dc_idx).matches_phys(phys_addr) { @@ -2202,12 +2273,12 @@ impl MipsCache for R4000Cache { self.mark_l1d_dirty(dc_idx); return BUS_OK; } + } // R5K (2-way): generic path. - #[cfg(feature = "r5k")] { let way = self.ensure_l1d_line(virt_addr, phys_addr); if way > 1 { return way; } - let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << DCache::NUM_LINES_SHIFT; + let dc_eidx = self.dc.get_index(virt_addr) | (way as usize) << Self::DC_NUM_LINES_SHIFT; let da = Self::dc_data_addr(dc_eidx, virt_addr); let current = self.dc.dc_read::<8>(da); self.dc.dc_write::<8>(da, (current & !mask) | (val & mask)); @@ -2247,15 +2318,17 @@ impl MipsCache for R4000Cache { let idx = if is_l2 { self.l2.get_index(phys_addr) } else if is_icache { - #[cfg(not(feature = "r5k"))] + if Self::IS_R5K { + { self.ic.get_index(virt_addr) | (((virt_addr >> 14) as usize & 1) << Self::IC_NUM_LINES_SHIFT) } + } else { { self.ic.get_index(virt_addr) } - #[cfg(feature = "r5k")] - { self.ic.get_index(virt_addr) | (((virt_addr >> 14) as usize & 1) << ICache::NUM_LINES_SHIFT) } + } } else { - #[cfg(not(feature = "r5k"))] + if Self::IS_R5K { + { self.dc.get_index(virt_addr) | (((virt_addr >> 14) as usize & 1) << Self::DC_NUM_LINES_SHIFT) } + } else { { self.dc.get_index(virt_addr) } - #[cfg(feature = "r5k")] - { self.dc.get_index(virt_addr) | (((virt_addr >> 14) as usize & 1) << DCache::NUM_LINES_SHIFT) } + } }; #[cfg(feature = "debug_cache")] @@ -2268,10 +2341,10 @@ impl MipsCache for R4000Cache { // For both L1I and L1D: fire on phys address match OR set index match self.is_tracking_addr(phys_addr, phys_addr) || self.is_tracking_l2_idx(self.l2.get_index(phys_addr)) - || self.is_tracking_l1d_idx(idx & DCache::NUM_LINES_MASK) + || self.is_tracking_l1d_idx(idx & Self::DC_NUM_LINES_MASK) }; if tracked { - let way = if !is_l2 { idx >> DCache::NUM_LINES_SHIFT } else { 0 }; + let way = if !is_l2 { idx >> Self::DC_NUM_LINES_SHIFT } else { 0 }; println!("[CACHE DEBUG] cache_op: {} virt={:#x} phys={:#x} idx=0x{:x} way={}", cache_op_name(cache_op), virt_addr, phys_addr, idx, way); } @@ -2280,9 +2353,6 @@ impl MipsCache for R4000Cache { // cascade: on R5K, L1 cache ops must propagate to L2 (and L2 to memory) because // the PROM only flushes L1 when SC=1, relying on hardware to keep L2 coherent. //let cascade = !is_l2; - #[cfg(feature = "r5k")] - let cascade = false; - #[cfg(not(feature = "r5k"))] let cascade = false; match operation { @@ -2498,7 +2568,7 @@ impl MipsCache for R4000Cache { match cache_target { CACH_PI => (IC_SIZE, IC_LINE), CACH_PD => (DC_SIZE, DC_LINE), - CACH_SI | CACH_SD => (L2_SIZE, L2_LINE), + CACH_SI | CACH_SD => (Self::L2_SIZE, L2_LINE), _ => (0, 16), } } @@ -2512,8 +2582,8 @@ impl MipsCache for R4000Cache { return; } let ll_addr = (self.get_lladdr() as u64) << 4; - let addr_line = phys_addr & !(DCache::LINE_MASK as u64); - let ll_line = ll_addr & !(DCache::LINE_MASK as u64); + let addr_line = phys_addr & !(Self::DC_LINE_MASK as u64); + let ll_line = ll_addr & !(Self::DC_LINE_MASK as u64); if addr_line == ll_line { self.set_llbit(false); } @@ -2539,7 +2609,7 @@ impl MipsCache for R4000Cache { match cache_name { "l1i" => { let set = self.ic.get_index(virt_addr); - let num_ways = ICache::NUM_LINES / (IC_SIZE / IC_LINE / IC_WAYS).max(1); + let num_ways = Self::IC_NUM_LINES / (IC_SIZE / IC_LINE / IC_WAYS).max(1); let sets_per_way = IC_SIZE / IC_LINE / num_ways.max(1); // Compute the overall verdict up front (any way hitting is a // HIT) so the very first line states it plainly — a per-way @@ -2568,7 +2638,7 @@ impl MipsCache for R4000Cache { } "l1d" => { let set = self.dc.get_index(virt_addr); - let num_ways = DCache::NUM_LINES / (DC_SIZE / DC_LINE / DC_WAYS).max(1); + let num_ways = Self::DC_NUM_LINES / (DC_SIZE / DC_LINE / DC_WAYS).max(1); let sets_per_way = DC_SIZE / DC_LINE / num_ways.max(1); // See l1i's own comment above — same "state the verdict up // front" fix. @@ -2632,14 +2702,13 @@ impl MipsCache for R4000Cache { return format!("Index 0x{:x} out of bounds (max 0x{:x})", idx, max_idx - 1); } let tag: L1ITag = self.ic.get_tag(idx); - let instrs_per_ic_line = ICache::INSTRS_PER_LINE; + let instrs_per_ic_line = Self::IC_INSTRS_PER_LINE; // R5K: instruction words live in ic_instrs (owned by L1I, indexed by eidx). // R4K: instruction words live in l2.instrs (indexed by physical word address). - #[cfg(feature = "r5k")] - let mut s = { + let mut s = if Self::IS_R5K { let ic_instrs = self.ic_instrs.get(); - let slot_base = idx << ICache::INSTR_SHIFT; + let slot_base = idx << Self::IC_INSTR_SHIFT; let way = idx / (IC_SIZE / IC_LINE / IC_WAYS); let set = idx % (IC_SIZE / IC_LINE / IC_WAYS); let mut s = format!("L1-I Line 0x{:x} (way={} set=0x{:x}): Tag=0x{:010x} V={}\n Instrs:", @@ -2651,12 +2720,10 @@ impl MipsCache for R4000Cache { } } s - }; - #[cfg(not(feature = "r5k"))] - let mut s = { + } else { let l2_data = self.l2.data(); let phys_base = tag.line_addr() as usize; - let l2_slot_base = (phys_base & (L2_SIZE - 1)) >> 2; + let l2_slot_base = (phys_base & (Self::L2_SIZE - 1)) >> 2; let mut s = format!("L1-I Line 0x{:x}: Tag=0x{:010x} V={}\n Instrs:", idx, tag.line_addr(), tag.is_valid()); for i in 0..instrs_per_ic_line { if i % 4 == 0 { s.push_str("\n "); } @@ -2682,10 +2749,10 @@ impl MipsCache for R4000Cache { let l2_data = self.l2.data(); let l2_idx = self.l2.get_index(tag.line_addr()); let l2_tag: L2Tag = self.l2.get_tag(l2_idx); - let l2_base = l2_idx << L2Cache::CHUNKS_PER_LINE_SHIFT; - let sub = ((tag.line_addr() as usize) & L2Cache::LINE_MASK) >> 3; + let l2_base = l2_idx << Self::L2_CHUNKS_PER_LINE_SHIFT; + let sub = ((tag.line_addr() as usize) & Self::L2_LINE_MASK) >> 3; s.push_str(&format!("\n L2[0x{:x}] cs={}: ", l2_idx, l2_tag.cs())); - for i in 0..ICache::CHUNKS_PER_LINE { + for i in 0..Self::IC_CHUNKS_PER_LINE { if l2_base + sub + i < l2_data.len() { s.push_str(&format!("{:016x} ", l2_data[l2_base + sub + i])); } @@ -2708,11 +2775,11 @@ impl MipsCache for R4000Cache { }; let dc_data = self.dc.data(); - let start = idx << DCache::CHUNKS_PER_LINE_SHIFT; + let start = idx << Self::DC_CHUNKS_PER_LINE_SHIFT; let mut s = format!("L1-D Line 0x{:x}: Tag=0x{:010x} CS={} ({}) D={}\n Data:", idx, tag.ptag, tag.cs, cs_str, tag.dirty); - for i in 0..DCache::CHUNKS_PER_LINE { + for i in 0..Self::DC_CHUNKS_PER_LINE { if i % 4 == 0 { s.push_str("\n "); } if start + i < dc_data.len() { s.push_str(&format!("{:016x} ", dc_data[start + i])); @@ -2721,8 +2788,8 @@ impl MipsCache for R4000Cache { s } "l2" => { - if L2Cache::NUM_LINES == 0 || idx >= L2Cache::NUM_LINES { - return format!("Index 0x{:x} out of bounds (max 0x{:x})", idx, L2Cache::NUM_LINES.saturating_sub(1)); + if Self::L2_NUM_LINES == 0 || idx >= Self::L2_NUM_LINES { + return format!("Index 0x{:x} out of bounds (max 0x{:x})", idx, Self::L2_NUM_LINES.saturating_sub(1)); } let tag: L2Tag = self.l2.get_tag(idx); let cs_str = match tag.cs() { @@ -2735,11 +2802,11 @@ impl MipsCache for R4000Cache { }; let l2_data = self.l2.data(); - let start = idx << L2Cache::CHUNKS_PER_LINE_SHIFT; + let start = idx << Self::L2_CHUNKS_PER_LINE_SHIFT; let mut s = format!("L2 Line 0x{:x}: Tag=0x{:05x} CS={} ({})\n Data:", idx, tag.ptag(), tag.cs(), cs_str); - for i in 0..L2Cache::CHUNKS_PER_LINE { + for i in 0..Self::L2_CHUNKS_PER_LINE { if i % 4 == 0 { s.push_str("\n "); } if start + i < l2_data.len() { s.push_str(&format!("{:016x} ", l2_data[start + i])); @@ -2757,15 +2824,17 @@ impl MipsCache for R4000Cache { self.dc.data_mut().fill(0); self.l2.tags_mut().fill(L2Tag::default()); self.l2.data_mut().fill(0); - #[cfg(not(feature = "r5k"))] - for s in self.l2.instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } - #[cfg(feature = "r5k")] + if Self::IS_R5K { for s in self.ic_instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } - #[cfg(feature = "r5k")] + } else { + for s in self.l2.instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } + } + if Self::IS_R5K { unsafe { (*self.ic_lru.get()).fill(0u64); (*self.dc_lru.get()).fill(0u64); } + } unsafe { *self.llbit.get() = false; *self.lladdr.get() = 0; @@ -2773,18 +2842,22 @@ impl MipsCache for R4000Cache { } fn save_cache_state(&self) -> toml::Value { - R4000Cache::save_cache_state(self) + Self::save_cache_state(self) } fn load_cache_state(&self, v: &toml::Value) -> Result<(), String> { - R4000Cache::load_cache_state(self, v) + Self::load_cache_state(self, v) } } // ---- Drop: stop and join decode thread ---- -impl Drop for R4000Cache { +impl Drop for CpuCache { fn drop(&mut self) { self.ic.stop.store(true, Ordering::Relaxed); } @@ -2792,22 +2865,28 @@ impl Drop for R4000Cache { // ---- Resettable ---- -impl Resettable for R4000Cache { +impl Resettable for CpuCache { fn power_on(&self) { self.ic.tags_mut().fill(L1ITag::default()); self.dc.tags_mut().fill(L1DTag::default()); self.dc.data_mut().fill(0); self.l2.tags_mut().fill(L2Tag::default()); self.l2.data_mut().fill(0); - #[cfg(not(feature = "r5k"))] - for s in self.l2.instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } - #[cfg(feature = "r5k")] + if Self::IS_R5K { for s in self.ic_instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } - #[cfg(feature = "r5k")] + } else { + for s in self.l2.instrs.get_mut().iter_mut() { s.flags = FLAG_NOT_DECODED; s.raw = 0; } + } + if Self::IS_R5K { unsafe { (*self.ic_lru.get()).fill(0u64); (*self.dc_lru.get()).fill(0u64); } + } unsafe { *self.llbit.get() = false; *self.lladdr.get() = 0; @@ -2817,7 +2896,11 @@ impl Resettable for R4000Cache { // ---- snapshot helpers + MipsCache save/load override ---- -impl R4000Cache { +impl CpuCache { fn save_tags_as_u32>(tags: &[TAG]) -> Vec { tags.iter().map(|&t| t.into()).collect() } @@ -2846,10 +2929,9 @@ impl R4000Cache { t.insert("l2_data".into(), u64_slice_to_toml(&l2_data)); t.insert("llbit".into(), toml::Value::Boolean(llbit)); t.insert("lladdr".into(), hex_u32(lladdr)); - // R5K: save LRU state as packed u32 words (1 bit per set, same on-disk format as before). + // 2-way only: LRU as packed u32 words, 1 bit per set (unchanged on-disk format). // ic_instrs not saved — rebuilt from l2.data on first fetch miss after restore. - #[cfg(feature = "r5k")] - { + if Self::IS_R5K { let pack = |lru: &[u64], num_sets: usize| -> Vec { (0..num_sets.div_ceil(32)).map(|i| { let base = i * 32; @@ -2862,18 +2944,18 @@ impl R4000Cache { }; let ic_lru = unsafe { &*self.ic_lru.get() }; let dc_lru = unsafe { &*self.dc_lru.get() }; - t.insert("ic_lru".into(), u32_slice_to_toml(&pack(ic_lru, IC_NUM_SETS))); - t.insert("dc_lru".into(), u32_slice_to_toml(&pack(dc_lru, DC_NUM_SETS))); + t.insert("ic_lru".into(), u32_slice_to_toml(&pack(ic_lru, Self::IC_NUM_SETS))); + t.insert("dc_lru".into(), u32_slice_to_toml(&pack(dc_lru, Self::DC_NUM_SETS))); } toml::Value::Table(t) } pub fn load_cache_state(&self, v: &toml::Value) -> Result<(), String> { - let mut ic_tags = vec![0u32; ICache::NUM_LINES]; - let mut dc_tags = vec![0u32; DCache::NUM_LINES]; + let mut ic_tags = vec![0u32; Self::IC_NUM_LINES]; + let mut dc_tags = vec![0u32; Self::DC_NUM_LINES]; let mut dc_data = vec![0u64; DC_SIZE / 8]; - let mut l2_tags = vec![0u32; L2Cache::NUM_LINES]; - let mut l2_data = vec![0u64; L2_SIZE / 8]; + let mut l2_tags = vec![0u32; Self::L2_NUM_LINES]; + let mut l2_data = vec![0u64; Self::L2_SIZE / 8]; if let Some(f) = get_field(v, "ic_tags") { load_u32_slice(f, &mut ic_tags); } if let Some(f) = get_field(v, "dc_tags") { load_u32_slice(f, &mut dc_tags); } @@ -2894,19 +2976,19 @@ impl R4000Cache { Self::load_tags_from_u32(self.l2.tags_mut(), &l2_tags); let dl = dc_data.len().min(DC_SIZE / 8); self.dc.data_mut()[..dl].copy_from_slice(&dc_data[..dl]); - let dl = l2_data.len().min(L2_SIZE / 8); + let dl = l2_data.len().min(Self::L2_SIZE / 8); self.l2.data_mut()[..dl].copy_from_slice(&l2_data[..dl]); // R4K: rebuild l2.instrs from restored l2.data; fetch() indexes it directly. // R5K: l2.instrs is empty; ic_instrs will be repopulated on next L1I miss. - #[cfg(not(feature = "r5k"))] + if !Self::IS_R5K { { let l2_data_slice = self.l2.data(); let l2_instrs = self.l2.instrs.get_mut(); - for line in 0..L2Cache::NUM_LINES { - let chunks_start = line << L2Cache::CHUNKS_PER_LINE_SHIFT; - let instrs_start = line << L2Cache::INSTR_SHIFT; - for i in 0..L2Cache::CHUNKS_PER_LINE { + for line in 0..Self::L2_NUM_LINES { + let chunks_start = line << Self::L2_CHUNKS_PER_LINE_SHIFT; + let instrs_start = line << Self::L2_INSTR_SHIFT; + for i in 0..Self::L2_CHUNKS_PER_LINE { let chunk = l2_data_slice[chunks_start + i]; l2_instrs[instrs_start + i * 2].raw = (chunk >> 32) as u32; l2_instrs[instrs_start + i * 2].flags = FLAG_NOT_DECODED; @@ -2915,10 +2997,10 @@ impl R4000Cache { } } } + } - // R5K: restore LRU bits; ic_instrs will be repopulated on first fetch miss. - #[cfg(feature = "r5k")] - { + // 2-way only: restore LRU bits; ic_instrs repopulates on the first fetch miss. + if Self::IS_R5K { let unpack = |packed: &[u32], dst: &mut [u64], num_sets: usize| { dst.fill(0); for set in 0..num_sets { @@ -2927,12 +3009,12 @@ impl R4000Cache { } } }; - let mut ic_lru_packed = vec![0u32; IC_NUM_SETS.div_ceil(32)]; - let mut dc_lru_packed = vec![0u32; DC_NUM_SETS.div_ceil(32)]; + let mut ic_lru_packed = vec![0u32; Self::IC_NUM_SETS.div_ceil(32)]; + let mut dc_lru_packed = vec![0u32; Self::DC_NUM_SETS.div_ceil(32)]; if let Some(f) = get_field(v, "ic_lru") { load_u32_slice(f, &mut ic_lru_packed); } if let Some(f) = get_field(v, "dc_lru") { load_u32_slice(f, &mut dc_lru_packed); } - unpack(&ic_lru_packed, unsafe { &mut *self.ic_lru.get() }, IC_NUM_SETS); - unpack(&dc_lru_packed, unsafe { &mut *self.dc_lru.get() }, DC_NUM_SETS); + unpack(&ic_lru_packed, unsafe { &mut *self.ic_lru.get() }, Self::IC_NUM_SETS); + unpack(&dc_lru_packed, unsafe { &mut *self.dc_lru.get() }, Self::DC_NUM_SETS); } if let Some(f) = get_field(v, "llbit") { @@ -2963,8 +3045,13 @@ mod tests { // Virtual address in kseg0; pidx bits[14:12] == 0 so R4K never fires VCE. fn kseg0(phys: u32) -> u64 { 0x8000_0000u64 | (phys as u64 & 0x0FFF_FFFF) } - fn make_cache(mem: Arc) -> R4000Cache { - R4000Cache::new(mem as Arc) + fn make_cache(mem: Arc) -> R4400Cache { + R4400Cache::new(mem as Arc) + } + + // Same helper for whichever CPU model a test wants to exercise. + fn make_cache_of>>(mem: Arc) -> C { + C::from(mem as Arc) } // xorshift64 — no external crate. @@ -2979,9 +3066,13 @@ mod tests { /// L1D random read/write: 1M word operations against a shadow copy. #[test] - fn l1d_random_stress() { + fn l1d_random_stress_r4400() { l1d_random_stress_for::() } + #[test] + fn l1d_random_stress_r5000() { l1d_random_stress_for::() } + + fn l1d_random_stress_for>>() { let mem = Arc::new(Memory::new(MEM_MB)); - let cache = make_cache(mem.clone()); + let cache: C = make_cache_of(mem.clone()); let mut rng = Rng::new(0xdeadbeef_cafebabe); let mut shadow = vec![0u32; MEM_BYTES / 4]; @@ -3004,16 +3095,16 @@ mod tests { // Index_WBInvalidate all L1D sets (both ways for R5K — way selected by addr bit 14), // then Index_WBInvalidate all L2 sets, so backing memory is fully up to date. - let dc_sets = DC_SIZE / DC_LINE / DC_WAYS; - for way in 0..DC_WAYS { + let dc_sets = C::DC_SIZE / C::DC_LINE / C::DC_WAYS; + for way in 0..C::DC_WAYS { for set in 0..dc_sets { // Bit 14 selects way for R5K index ops; for R4K WAYS==1 so way is always 0. - let virt = kseg0(((way << 14) | (set * DC_LINE)) as u32); + let virt = kseg0(((way << 14) | (set * C::DC_LINE)) as u32); cache.cache_op(C_IINV | CACH_PD, virt, virt & 0x1FFF_FFFF); } } - for i in 0..(L2_SIZE / L2_LINE) { - let phys = (i * L2_LINE) as u64; + for i in 0..(C::L2_SIZE / C::L2_LINE) { + let phys = (i * C::L2_LINE) as u64; cache.cache_op(C_IWBINV | CACH_SD, phys, phys); } for (i, &want) in shadow.iter().enumerate() { @@ -3026,7 +3117,11 @@ mod tests { /// L1I fetch stress: 1M random fetches against memory pre-filled with known words. #[test] - fn l1i_fetch_stress() { + fn l1i_fetch_stress_r4400() { l1i_fetch_stress_for::() } + #[test] + fn l1i_fetch_stress_r5000() { l1i_fetch_stress_for::() } + + fn l1i_fetch_stress_for>>() { let mem = Arc::new(Memory::new(MEM_MB)); // Pre-fill with deterministic pattern directly through the bus. let mut rng = Rng::new(0x1234_5678_9abc_def0); @@ -3036,7 +3131,7 @@ mod tests { mem.write32((i * 4) as u32, *w); } - let cache = make_cache(mem.clone()); + let cache: C = make_cache_of(mem.clone()); let mut rng2 = Rng::new(0xfeed_face_dead_beef); for op in 0..1_000_000 { @@ -3058,27 +3153,27 @@ mod tests { // Flush the entire cache hierarchy to backing memory and invalidate // everything, so tests start from a clean slate. - fn full_flush(cache: &R4000Cache) { + fn full_flush(cache: &R4400Cache) { // Index_WBInvalidate all L1D sets (both ways for R5K). - for way in 0..DC_WAYS { - for set in 0..DC_SIZE / DC_LINE / DC_WAYS { - let nls = DCache::NUM_LINES_SHIFT as usize; - let ls = DCache::LINE_SHIFT as usize; + for way in 0..R4400Cache::DC_WAYS { + for set in 0..R4400Cache::DC_SIZE / R4400Cache::DC_LINE / R4400Cache::DC_WAYS { + let nls = R4400Cache::DC_NUM_LINES_SHIFT as usize; + let ls = R4400Cache::DC_LINE_SHIFT as usize; let idx_addr = (way << (nls + ls)) | (set << ls); let v = kseg0(idx_addr as u32); cache.cache_op(C_IWBINV | CACH_PD, v, v & 0x1FFF_FFFF); } } // Index_WBInvalidate all L2 sets (single-way, physically indexed). - for set in 0..L2_SIZE / L2_LINE { - let p = (set * L2_LINE) as u64; + for set in 0..R4400Cache::L2_SIZE / R4400Cache::L2_LINE { + let p = (set * R4400Cache::L2_LINE) as u64; cache.cache_op(C_IWBINV | CACH_SD, p, p); } // Index_Invalidate all L1I sets (both ways for R5K). - for way in 0..IC_WAYS { - for set in 0..IC_SIZE / IC_LINE / IC_WAYS { - let nls = ICache::NUM_LINES_SHIFT as usize; - let ls = ICache::LINE_SHIFT as usize; + for way in 0..R4400Cache::IC_WAYS { + for set in 0..R4400Cache::IC_SIZE / R4400Cache::IC_LINE / R4400Cache::IC_WAYS { + let nls = R4400Cache::IC_NUM_LINES_SHIFT as usize; + let ls = R4400Cache::IC_LINE_SHIFT as usize; let idx_addr = (way << (nls + ls)) | (set << ls); let v = kseg0(idx_addr as u32); cache.cache_op(C_IINV | CACH_PI, v, v & 0x1FFF_FFFF); @@ -3092,10 +3187,9 @@ mod tests { // Flush a single L2 set to memory. `phys` is any address within the L2 line. // Only valid when L2 is present (r4k or r5k+r5ksc). - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] - fn flush_l2_to_mem(cache: &R4000Cache, phys: u32) { - let l2_set = (phys as usize >> L2Cache::LINE_SHIFT as usize) & (L2_SIZE / L2_LINE - 1); - let lp = (l2_set * L2_LINE) as u64; + fn flush_l2_to_mem(cache: &R4400Cache, phys: u32) { + let l2_set = (phys as usize >> R4400Cache::L2_LINE_SHIFT as usize) & (R4400Cache::L2_SIZE / R4400Cache::L2_LINE - 1); + let lp = (l2_set * R4400Cache::L2_LINE) as u64; cache.cache_op(C_IWBINV | CACH_SD, lp, lp); } @@ -3103,7 +3197,6 @@ mod tests { /// Verify: data reaches L2 (by also flushing L2 to memory), and tag is invalidated /// (subsequent read after memory update picks up new value). #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_index_wbinv_l1d() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3113,8 +3206,8 @@ mod tests { let _ = cache.write::<4>(virt, phys as u64, 0xABCD_1234u64); // Index_WBInvalidate the L1D set (way 0 — bit14 of index address = 0). - let set = (phys as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK; - let v0 = kseg0((set << DCache::LINE_SHIFT as usize) as u32); + let set = (phys as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK; + let v0 = kseg0((set << R4400Cache::DC_LINE_SHIFT as usize) as u32); cache.cache_op(C_IWBINV | CACH_PD, v0, v0 & 0x1FFF_FFFF); // Data should be in L2 now. Flush L2 → memory and verify. @@ -3133,7 +3226,6 @@ mod tests { /// L1I fill path goes through L2, so we must also update L2 to see new memory /// content; OR use addresses where L2 is also cold. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_index_inv_l1i() { let mem = Arc::new(Memory::new(MEM_MB)); // Pre-fill memory with a pattern. @@ -3157,8 +3249,8 @@ mod tests { assert_eq!(stale, 0x1111_1111, "expected stale L1I hit before invalidate"); // Index_Invalidate L1I (way 0 — bit14=0 in index address). - let set = (phys as usize >> ICache::LINE_SHIFT as usize) & ICache::NUM_LINES_MASK; - let iv = kseg0((set << ICache::LINE_SHIFT as usize) as u32); + let set = (phys as usize >> R4400Cache::IC_LINE_SHIFT as usize) & R4400Cache::IC_NUM_LINES_MASK; + let iv = kseg0((set << R4400Cache::IC_LINE_SHIFT as usize) as u32); cache.cache_op(C_IINV | CACH_PI, iv, iv & 0x1FFF_FFFF); // L1I tag is now invalid. But L2 still caches the old value — also invalidate L2 @@ -3177,7 +3269,6 @@ mod tests { /// Hit_WBInvalidate L1D: flushes dirty line to L2 and invalidates tag. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_hit_wbinv_l1d() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3208,7 +3299,6 @@ mod tests { /// Hit_Invalidate L1D: invalidates L1D without writeback. /// The clean line is simply dropped; L2 still holds the original value. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_hit_inv_l1d() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3229,7 +3319,6 @@ mod tests { /// Index_WBInvalidate L2: flushes and invalidates an L2 line. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_index_wbinv_l2() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3239,18 +3328,18 @@ mod tests { // Write into cache — lands in both L1D and L2. let _ = cache.write::<4>(virt, phys as u64, 0x5555_AAAA_u64); // Flush L1D first (Index_WBInv), so data propagates to L2. - let dc_set = (phys as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK; - for way in 0..DC_WAYS { - let nls = DCache::NUM_LINES_SHIFT as usize; - let ls = DCache::LINE_SHIFT as usize; + let dc_set = (phys as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK; + for way in 0..R4400Cache::DC_WAYS { + let nls = R4400Cache::DC_NUM_LINES_SHIFT as usize; + let ls = R4400Cache::DC_LINE_SHIFT as usize; let idx_addr = (way << (nls + ls)) | (dc_set << ls); let v = kseg0(idx_addr as u32); cache.cache_op(C_IWBINV | CACH_PD, v, v & 0x1FFF_FFFF); } // Now Index_WBInvalidate the L2 line — flushes L2 dirty data to memory. - let l2_set = (phys as usize >> L2Cache::LINE_SHIFT as usize) & (L2_SIZE / L2_LINE - 1); - let lp = (l2_set * L2_LINE) as u64; + let l2_set = (phys as usize >> R4400Cache::L2_LINE_SHIFT as usize) & (R4400Cache::L2_SIZE / R4400Cache::L2_LINE - 1); + let lp = (l2_set * R4400Cache::L2_LINE) as u64; cache.cache_op(C_IWBINV | CACH_SD, lp, lp); // Memory should now have the value. @@ -3266,7 +3355,6 @@ mod tests { /// On R4K (inclusive): also cascades to L1D. /// On R5K (non-inclusive): L1D is unaffected. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_hit_inv_l2() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3283,20 +3371,14 @@ mod tests { // Overwrite memory. mem_write(&mem, phys, 0x1234_5678); - // On R4K, L2 invalidation cascades to L1D → next read refills from updated memory. - // On R5K, L1D is unaffected → still holds old value 0xDECA_FBAD (from the read above). + // R4400 L2 is inclusive: invalidation cascades to L1D, so the read refills. let r = cache.read::<4>(virt, phys as u64); - #[cfg(not(feature = "r5k"))] assert_eq!(r.data as u32, 0x1234_5678, - "Hit_Inv(SD) did not cascade invalidation to L1D (R4K inclusive)"); - #[cfg(feature = "r5k")] - assert_eq!(r.data as u32, 0xDECA_FBAD, - "Hit_Inv(SD) evicted L1D on R5K (non-inclusive — L1 should be unaffected)"); + "Hit_Inv(SD) did not cascade invalidation to L1D (inclusive L2)"); } /// Hit_WBInvalidate L2: writes back dirty L2 line to memory. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_hit_wbinv_l2() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3306,10 +3388,10 @@ mod tests { // Write into cache (dirty in L1D; L2 gets dirty during L1D writeback). let _ = cache.write::<4>(virt, phys as u64, 0x1122_3344_u64); // Flush L1D to make L2 dirty. - let dc_set = (phys as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK; - for way in 0..DC_WAYS { - let nls = DCache::NUM_LINES_SHIFT as usize; - let ls = DCache::LINE_SHIFT as usize; + let dc_set = (phys as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK; + for way in 0..R4400Cache::DC_WAYS { + let nls = R4400Cache::DC_NUM_LINES_SHIFT as usize; + let ls = R4400Cache::DC_LINE_SHIFT as usize; let idx_addr = (way << (nls + ls)) | (dc_set << ls); let v = kseg0(idx_addr as u32); cache.cache_op(C_IWBINV | CACH_PD, v, v & 0x1FFF_FFFF); @@ -3328,7 +3410,6 @@ mod tests { /// Index_LoadTag / Index_StoreTag round-trip: stored tag must read back identically. #[test] - #[cfg(not(all(feature = "r5k", not(feature = "r5ksc"))))] fn cache_op_ilt_ist_l2() { let mem = Arc::new(Memory::new(MEM_MB)); let cache = make_cache(mem.clone()); @@ -3338,8 +3419,8 @@ mod tests { // Populate L2 by doing a read. let _ = cache.read::<4>(virt, phys as u64); - let l2_set = (phys as usize >> L2Cache::LINE_SHIFT as usize) & (L2_SIZE / L2_LINE - 1); - let lp = (l2_set * L2_LINE) as u64; + let l2_set = (phys as usize >> R4400Cache::L2_LINE_SHIFT as usize) & (R4400Cache::L2_SIZE / R4400Cache::L2_LINE - 1); + let lp = (l2_set * R4400Cache::L2_LINE) as u64; // Index_LoadTag — read current tag. let tag_lo_read = cache.cache_op(C_ILT | CACH_SD, lp, lp); @@ -3368,8 +3449,8 @@ mod tests { let phys1: u32 = 0x0000_5000; // set 0x80, bit14=1 // Verify same set and different bit14. assert_eq!( - (phys0 as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK, - (phys1 as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK, + (phys0 as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK, + (phys1 as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK, "phys0 and phys1 must map to the same L1D set" ); assert_ne!(phys1 & (1 << 14), 0, "phys1 must have bit14=1"); @@ -3385,8 +3466,8 @@ mod tests { let _ = cache.read::<4>(virt1, phys1 as u64); // Index_WBInvalidate using index address with bit14=0 — should evict way0. - let set = (phys0 as usize >> DCache::LINE_SHIFT as usize) & DCache::NUM_LINES_MASK; - let inv0 = kseg0((set << DCache::LINE_SHIFT as usize) as u32); // bit14=0 + let set = (phys0 as usize >> R4400Cache::DC_LINE_SHIFT as usize) & R4400Cache::DC_NUM_LINES_MASK; + let inv0 = kseg0((set << R4400Cache::DC_LINE_SHIFT as usize) as u32); // bit14=0 assert_eq!(inv0 & (1 << 14), 0, "inv0 must have bit14=0"); cache.cache_op(C_IWBINV | CACH_PD, inv0, inv0 & 0x1FFF_FFFF); @@ -3396,21 +3477,22 @@ mod tests { assert_eq!(r0.data as u32, 0xAAAA_0001, "Way0 not invalidated by Index_WBInv with bit14=0"); // Way1 should be unaffected — still holds 0xBBBB_0002. - #[cfg(feature = "r5k")] + if R4400Cache::IS_R5K { { let r1 = cache.read::<4>(virt1, phys1 as u64); assert_eq!(r1.data as u32, 0xBBBB_0002, "Way1 was incorrectly evicted by Index_WBInv targeting way0"); } - #[cfg(not(feature = "r5k"))] + } else { { // R4K single-way: both addresses alias, result is implementation-defined. let _ = cache.read::<4>(virt1, phys1 as u64); } + } // Index_Invalidate L1I using bit14=1 address — should not affect way0 L1I line. - let ic_set = (phys1 as usize >> ICache::LINE_SHIFT as usize) & ICache::NUM_LINES_MASK; - let inv1 = kseg0(((ic_set << ICache::LINE_SHIFT as usize) | (1 << 14)) as u32); // bit14=1 + let ic_set = (phys1 as usize >> R4400Cache::IC_LINE_SHIFT as usize) & R4400Cache::IC_NUM_LINES_MASK; + let inv1 = kseg0(((ic_set << R4400Cache::IC_LINE_SHIFT as usize) | (1 << 14)) as u32); // bit14=1 cache.cache_op(C_IINV | CACH_PI, inv1, inv1 & 0x1FFF_FFFF); // No panic = test passes. } @@ -3467,17 +3549,17 @@ mod tests { shadow[(phys / 4) as usize] = val; // Index_WBInv L1D both ways: set index = (phys >> LINE_SHIFT) & NUM_LINES_MASK, // way selected by bit (LINE_SHIFT + log2(NUM_SETS)) = bit 14 for both L1D and L1I. - let dc_set = ((phys as usize) >> DCache::LINE_SHIFT) & DCache::NUM_LINES_MASK; - let dc_v0 = kseg0((dc_set << DCache::LINE_SHIFT as usize) as u32); - let dc_v1 = kseg0(((dc_set << DCache::LINE_SHIFT as usize) | (1 << 14)) as u32); + let dc_set = ((phys as usize) >> R4400Cache::DC_LINE_SHIFT) & R4400Cache::DC_NUM_LINES_MASK; + let dc_v0 = kseg0((dc_set << R4400Cache::DC_LINE_SHIFT as usize) as u32); + let dc_v1 = kseg0(((dc_set << R4400Cache::DC_LINE_SHIFT as usize) | (1 << 14)) as u32); cache.cache_op(C_IINV | CACH_PD, dc_v0, dc_v0 & 0x1FFF_FFFF); cache.cache_op(C_IINV | CACH_PD, dc_v1, dc_v1 & 0x1FFF_FFFF); // L2 hit-writeback (single-way, hit op fine). cache.cache_op(C_HWBINV | CACH_SD, phys as u64, phys as u64); // Index_Inv L1I both ways. - let ic_set = ((phys as usize) >> ICache::LINE_SHIFT) & ICache::NUM_LINES_MASK; - let ic_v0 = kseg0((ic_set << ICache::LINE_SHIFT as usize) as u32); - let ic_v1 = kseg0(((ic_set << ICache::LINE_SHIFT as usize) | (1 << 14)) as u32); + let ic_set = ((phys as usize) >> R4400Cache::IC_LINE_SHIFT) & R4400Cache::IC_NUM_LINES_MASK; + let ic_v0 = kseg0((ic_set << R4400Cache::IC_LINE_SHIFT as usize) as u32); + let ic_v1 = kseg0(((ic_set << R4400Cache::IC_LINE_SHIFT as usize) | (1 << 14)) as u32); cache.cache_op(C_IINV | CACH_PI, ic_v0, ic_v0 & 0x1FFF_FFFF); cache.cache_op(C_IINV | CACH_PI, ic_v1, ic_v1 & 0x1FFF_FFFF); // Verify flush landed in memory. diff --git a/src/mips_core.rs b/src/mips_core.rs index bd3dd8c..4f7c83e 100644 --- a/src/mips_core.rs +++ b/src/mips_core.rs @@ -555,6 +555,9 @@ pub struct MipsCore { pub cp0_errorepc: u64, // 30: Error Exception PC pub tlb_entries: u32, // Total TLB entries + /// CP0 PRId / CP1 FIR reset values, supplied by the CPU model at construction. + pub reset_prid: u32, + pub reset_fir: u32, pub cp0_random_cycle: u64, // Cycle count of last Random update /// Counts every CP0 Count==Compare match (i.e. every fastick interrupt). @@ -906,6 +909,8 @@ impl MipsCore { cp0_taghi: 0, cp0_errorepc: 0, tlb_entries: 48, + reset_prid: 0x0000_0440, + reset_fir: 0x0000_0500, cp0_random_cycle: 0, fasttick_count: Arc::new(AtomicU64::new(0)), running: false, @@ -967,10 +972,7 @@ impl MipsCore { self.disarm_compare_timer(); self.cp0_random = self.tlb_entries - 1; self.cp0_random_cycle = 0; - #[cfg(not(feature = "r5k"))] - { self.cp0_prid = 0x00000440; } // R4400, imp=0x04, majrev=4, minrev=0 - #[cfg(feature = "r5k")] - { self.cp0_prid = 0x00002321; } // R5000, imp=0x23, rev=2.1 + self.cp0_prid = self.reset_prid; self.cp0_watchlo = 0; self.cp0_watchhi = 0; self.cp0_xcontext = 0; @@ -980,10 +982,7 @@ impl MipsCore { self.cp0_taghi = 0; // CP1 registers - #[cfg(not(feature = "r5k"))] - { self.fpu_fir = 0x00000500; } // R4000 FPU: imp=0x05, rev=0 - #[cfg(feature = "r5k")] - { self.fpu_fir = 0x00002300; } // R5000 FPU: imp=0x23, rev=0 + self.fpu_fir = self.reset_fir; self.fpu_fccr = 0; self.fpu_fexr = 0; self.fpu_fenr = 0; diff --git a/src/mips_exec.rs b/src/mips_exec.rs index 16c42d5..ddba6aa 100644 --- a/src/mips_exec.rs +++ b/src/mips_exec.rs @@ -909,7 +909,7 @@ impl MipsCpuConfig { } /// MIPS Execution Engine - combines CPU core with memory interface and TLB -pub struct MipsExecutor { +pub struct MipsExecutor { pub core: MipsCore, pub sysad: Arc, /// cheritest convention (`--cheritest-dump-hook`): a guest write to CP0 26 @@ -1182,22 +1182,22 @@ fn bp_match_key(addr: u64) -> u64 { // ---- translate_fn slow-path wrappers (one per privilege × addressing-mode combination) ------ // These are free functions so they can be stored as bare fn pointers in MipsExecutor. // They are only called on a nanotlb miss — the nanotlb probe happens before the fn-pointer call. -fn translate_32_kernel(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_32_kernel(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_32bit_impl::(va, at) } -fn translate_32_supervisor(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_32_supervisor(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_32bit_impl::(va, at) } -fn translate_32_user(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_32_user(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_32bit_impl::(va, at) } -fn translate_64_kernel(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_64_kernel(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_64bit_impl::(va, at) } -fn translate_64_supervisor(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_64_supervisor(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_64bit_impl::(va, at) } -fn translate_64_user(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { +fn translate_64_user(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { e.translate_64bit_impl::(va, at) } @@ -1205,10 +1205,10 @@ fn translate_64_user(e: &mut MipsExecutor, va: u64, a /// Rust does not allow `Self` inside a nested fn, so the generic trampoline lives here. // Safety: the executor's raw pointers point into allocations owned by // MipsCore which outlive the executor. The executor is only accessed from the CPU thread. -unsafe impl Send for MipsExecutor {} -unsafe impl Sync for MipsExecutor {} +unsafe impl Send for MipsExecutor {} +unsafe impl Sync for MipsExecutor {} -fn mips_executor_status_cb(ctx: *mut core::ffi::c_void, old: u32, new: u32) { +fn mips_executor_status_cb(ctx: *mut core::ffi::c_void, old: u32, new: u32) { // SAFETY: ctx is `&mut MipsExecutor` cast to void, alive for the executor's lifetime, // and only ever called from the CPU thread that exclusively owns the executor. let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; @@ -1224,7 +1224,7 @@ fn mips_executor_status_cb(ctx: *mut core::ffi::c_void, ol // executor's own address, established by `install_jit_hooks` — same safety // argument as `mips_executor_status_cb` above. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_read8(ctx: *mut core::ffi::c_void, va: u64) -> u64 { +unsafe extern "C" fn jit_read8(ctx: *mut core::ffi::c_void, va: u64) -> u64 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(feature = "jitv2_lockstep")] { return exec.lockstep_jit_read::<1>(va); } @@ -1235,7 +1235,7 @@ unsafe extern "C" fn jit_read8(ctx: *mut core::ffi::c_void } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_read16(ctx: *mut core::ffi::c_void, va: u64) -> u64 { +unsafe extern "C" fn jit_read16(ctx: *mut core::ffi::c_void, va: u64) -> u64 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(feature = "jitv2_lockstep")] { return exec.lockstep_jit_read::<2>(va); } @@ -1246,7 +1246,7 @@ unsafe extern "C" fn jit_read16(ctx: *mut core::ffi::c_voi } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_read32(ctx: *mut core::ffi::c_void, va: u64) -> u64 { +unsafe extern "C" fn jit_read32(ctx: *mut core::ffi::c_void, va: u64) -> u64 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(feature = "jitv2_lockstep")] { return exec.lockstep_jit_read::<4>(va); } @@ -1257,7 +1257,7 @@ unsafe extern "C" fn jit_read32(ctx: *mut core::ffi::c_voi } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_read64(ctx: *mut core::ffi::c_void, va: u64) -> u64 { +unsafe extern "C" fn jit_read64(ctx: *mut core::ffi::c_void, va: u64) -> u64 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(feature = "jitv2_lockstep")] { return exec.lockstep_jit_read::<8>(va); } @@ -1268,7 +1268,7 @@ unsafe extern "C" fn jit_read64(ctx: *mut core::ffi::c_voi } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_write8(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { +unsafe extern "C" fn jit_write8(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; let val = val as u8; // mask to the real width ourselves — see write8_fn's doc comment on why the FFI parameter is u64 #[cfg(feature = "jitv2_lockstep")] @@ -1277,7 +1277,7 @@ unsafe extern "C" fn jit_write8(ctx: *mut core::ffi::c_voi { let status = exec.write_data::<1>(va, val as u64); exec.core.jit_mem_exc = status; status } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_write16(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { +unsafe extern "C" fn jit_write16(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; let val = val as u16; // mask to the real width ourselves — see write16_fn's doc comment #[cfg(feature = "jitv2_lockstep")] @@ -1286,7 +1286,7 @@ unsafe extern "C" fn jit_write16(ctx: *mut core::ffi::c_vo { let status = exec.write_data::<2>(va, val as u64); exec.core.jit_mem_exc = status; status } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_write32(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { +unsafe extern "C" fn jit_write32(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; let val = val as u32; // mask to the real width ourselves — see write32_fn's doc comment #[cfg(feature = "jitv2_lockstep")] @@ -1295,7 +1295,7 @@ unsafe extern "C" fn jit_write32(ctx: *mut core::ffi::c_vo { let status = exec.write_data::<4>(va, val as u64); exec.core.jit_mem_exc = status; status } } #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_write64(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { +unsafe extern "C" fn jit_write64(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(feature = "jitv2_lockstep")] { return exec.lockstep_jit_write::<8>(va, val); } @@ -1314,7 +1314,7 @@ unsafe extern "C" fn jit_write64(ctx: *mut core::ffi::c_vo /// the normal dispatch path, so this always goes straight through /// `write_data64_masked` regardless of that feature. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_write64_masked(ctx: *mut core::ffi::c_void, va: u64, val: u64, mask: u64) -> u32 { +unsafe extern "C" fn jit_write64_masked(ctx: *mut core::ffi::c_void, va: u64, val: u64, mask: u64) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; let status = exec.write_data64_masked(va, val, mask); exec.core.jit_mem_exc = status; @@ -1324,7 +1324,7 @@ unsafe extern "C" fn jit_write64_masked(ctx: *mut core::ff /// own `handle_exception` — the only place EPC/Cause/BD/vectoring are ever /// computed, for both engines. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_handle_exception(ctx: *mut core::ffi::c_void, status: u32) -> u32 { +unsafe extern "C" fn jit_handle_exception(ctx: *mut core::ffi::c_void, status: u32) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; // handle_exception reads self.core.in_delay_slot — a single MipsCore // field shared by both engines (no separate JIT-only copy or sync step @@ -1345,7 +1345,7 @@ unsafe extern "C" fn jit_handle_exception(ctx: *mut core:: /// can't tell "please retry me through the interpreter" apart from a real /// retirement, since both return `EXEC_COMPLETE`). #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_interp_fallback(ctx: *mut core::ffi::c_void) -> u32 { +unsafe extern "C" fn jit_interp_fallback(ctx: *mut core::ffi::c_void) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; exec.interp_dispatch_one() } @@ -1361,7 +1361,7 @@ unsafe extern "C" fn jit_interp_fallback(ctx: *mut core::f /// semantics) — `jitv2_track_pcp` only re-derives `self.pcp` on a Fetch /// nanotlb miss, which a same-page guard check can't trigger. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_kill_entry(ctx: *mut core::ffi::c_void, offset: u32) { +unsafe extern "C" fn jit_kill_entry(ctx: *mut core::ffi::c_void, offset: u32) { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; assert!(!exec.pcp.is_null(), "jit_kill_entry reached with no tracked PhysicalCodePage"); let page = unsafe { &*exec.pcp }; @@ -1393,7 +1393,7 @@ unsafe extern "C" fn jit_kill_entry(ctx: *mut core::ffi::c pub static DEV_TRACE_BP_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); #[cfg(all(feature = "jitv2", feature = "developer"))] -unsafe extern "C" fn jit_dev_trace_bp(ctx: *mut core::ffi::c_void, pc: u64, raw: u32, origin: u32) -> u32 { +unsafe extern "C" fn jit_dev_trace_bp(ctx: *mut core::ffi::c_void, pc: u64, raw: u32, origin: u32) -> u32 { DEV_TRACE_BP_CALLS.fetch_add(1, Ordering::Relaxed); let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; #[cfg(not(feature = "lightning"))] @@ -1431,7 +1431,7 @@ unsafe extern "C" fn jit_fpu_set_mode(_ctx: *mut core::ffi::c_void, rm: u32) { /// conversion instruction races the flag write against the read on /// out-of-order hardware. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_cvt_to_int( +unsafe extern "C" fn jit_cvt_to_int( ctx: *mut core::ffi::c_void, fs_reg: u32, fd_reg: u32, @@ -1447,7 +1447,7 @@ unsafe extern "C" fn jit_cvt_to_int( /// Single-implementation CVT.S.W/D.W/S.L/D.L — see `cvt_int_to_float_and_commit`'s /// doc comment. Same `ctx`-is-`MipsExecutor` shape as `jit_cvt_to_int`. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_cvt_int_to_float( +unsafe extern "C" fn jit_cvt_int_to_float( ctx: *mut core::ffi::c_void, fs_reg: u32, fd_reg: u32, @@ -1462,7 +1462,7 @@ unsafe extern "C" fn jit_cvt_int_to_float( /// Single-implementation CVT.S.D — see `cvt_d_to_s_and_commit`'s doc /// comment. Same `ctx`-is-`MipsExecutor` shape as `jit_cvt_to_int`. #[cfg(feature = "jitv2")] -unsafe extern "C" fn jit_cvt_d_to_s(ctx: *mut core::ffi::c_void, fs_reg: u32, fd_reg: u32, fr1: u32) -> u32 { +unsafe extern "C" fn jit_cvt_d_to_s(ctx: *mut core::ffi::c_void, fs_reg: u32, fd_reg: u32, fr1: u32) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; cvt_d_to_s_and_commit(&mut exec.core, fs_reg, fd_reg, fr1 != 0) as u32 } @@ -1475,7 +1475,7 @@ unsafe extern "C" fn jit_cvt_d_to_s(ctx: *mut core::ffi::c /// `MipsCore::lockstep_step_fn`. No-op (returns without comparing) for a /// `LockstepClass` that can't be compared per-dispatch (Branch/Other). #[cfg(feature = "jitv2_lockstep")] -unsafe extern "C" fn jit_lockstep_step(ctx: *mut core::ffi::c_void, pc: u64, raw: u32, bd: u32) { +unsafe extern "C" fn jit_lockstep_step(ctx: *mut core::ffi::c_void, pc: u64, raw: u32, bd: u32) { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; exec.lockstep_step(pc, raw, bd); } @@ -1488,7 +1488,7 @@ unsafe extern "C" fn jit_lockstep_step(ctx: *mut core::ffi /// Returns `EXEC_COMPLETE` when equal or when there was nothing to compare /// (disabled class / non-retiring reference). #[cfg(feature = "jitv2_lockstep")] -unsafe extern "C" fn jit_lockstep_compare(ctx: *mut core::ffi::c_void) -> u32 { +unsafe extern "C" fn jit_lockstep_compare(ctx: *mut core::ffi::c_void) -> u32 { let exec = unsafe { &mut *(ctx as *mut MipsExecutor) }; if exec.lockstep_compare() { EXEC_BREAKPOINT } else { EXEC_COMPLETE } } @@ -1878,7 +1878,7 @@ fn cvt_d_to_s_and_commit(core: &mut MipsCore, fs_reg: u32, fd_reg: u32, fr1: boo false } -impl MipsExecutor { +impl MipsExecutor { /// Create a new executor from a config and a bus (sysad) and a TLB. /// The cache hierarchy is constructed internally as a unified R4000Cache. pub fn new(sysad: Arc, tlb: T, cfg: &MipsCpuConfig) -> Self @@ -1887,8 +1887,7 @@ impl MipsExecutor { { let mut core = MipsCore::new(); - // Build unified cache hierarchy. Cache geometry is fixed at compile time; - // IC_SIZE/IC_LINE/DC_SIZE/DC_LINE/L2_SIZE/L2_LINE are consts from mips_cache_v2. + // Cache geometry comes from the model type C, so Config describes this CPU. // `mut` is only used by the Triton L2-enable sync below. #[cfg_attr(not(feature = "r5ksc_triton"), allow(unused_mut))] let mut cache = C::from(sysad.clone()); @@ -1900,16 +1899,16 @@ impl MipsExecutor { config |= 3 << CONFIG_K0; // DB (bit 4): Primary D-cache line size. 0=16B, 1=32B. - config |= (if DC_LINE >= 32 { 1 } else { 0 }) << CONFIG_DB; + config |= (if C::DC_LINE >= 32 { 1 } else { 0 }) << CONFIG_DB; // IB (bit 5): Primary I-cache line size. 0=16B, 1=32B. - config |= (if IC_LINE >= 32 { 1 } else { 0 }) << CONFIG_IB; + config |= (if C::IC_LINE >= 32 { 1 } else { 0 }) << CONFIG_IB; // DC (bits 8:6): Primary D-cache size. size = 2^(12+DC) - config |= DC_SIZE.trailing_zeros().saturating_sub(12) << CONFIG_DC; + config |= C::DC_SIZE.trailing_zeros().saturating_sub(12) << CONFIG_DC; // IC (bits 11:9): Primary I-cache size. size = 2^(12+IC) - config |= IC_SIZE.trailing_zeros().saturating_sub(12) << CONFIG_IC; + config |= C::IC_SIZE.trailing_zeros().saturating_sub(12) << CONFIG_IC; // BE (bit 15): Big Endian. 1 for Indy. config |= 1 << CONFIG_BE; @@ -1923,16 +1922,11 @@ impl MipsExecutor { // r5k without r5ksc: SC=1 — no L2 present; PROM uses 2-way index flush. // r5k + r5ksc (external): SC=1 — external cache sized via EEPROM; 2-way flush. // R4K: SC=0 when L2 present, SC=1 when absent. - #[cfg(feature = "r5ksc_triton")] - { config |= 0 << CONFIG_SC; } // Triton: SC=0, integrated L2 present - #[cfg(all(feature = "r5k", not(feature = "r5ksc_triton")))] - { config |= 1 << CONFIG_SC; } // R5K non-Triton: SC=1, PROM uses 2-way index flush - #[cfg(not(feature = "r5k"))] - { config |= (if L2_SIZE > 0 { 0 } else { 1 }) << CONFIG_SC; } + config |= (if C::L2_SIZE > 0 { 0 } else { 1 }) << CONFIG_SC; // SB (bits 23:22): Secondary cache block size. // 00=4 words (16B), 01=8 words (32B), 10=16 words (64B), 11=32 words (128B). - config |= (match L2_LINE { + config |= (match C::L2_LINE { 16 => 0b00, 32 => 0b01, 64 => 0b10, @@ -1945,7 +1939,7 @@ impl MipsExecutor { // IP32 (O2) reads CONFIG[21:20] directly — must match for correct L2 detection. #[cfg(feature = "r5ksc_triton")] { - let ss: u32 = match L2_SIZE { + let ss: u32 = match C::L2_SIZE { 524288 => 0b00, // 512 KB 1048576 => 0b01, // 1 MB 2097152 => 0b10, // 2 MB @@ -1955,7 +1949,9 @@ impl MipsExecutor { } core.cp0_config = config; - core.tlb_entries = cfg.tlb_entries as u32; + core.tlb_entries = C::TLB_ENTRIES as u32; + core.reset_prid = C::PRID; + core.reset_fir = C::FIR; // Triton: sync initial L2 enabled state from Config SE bit (starts 0 = disabled). #[cfg(feature = "r5ksc_triton")] @@ -7480,7 +7476,7 @@ impl LockstepSnapshot { /// to restore real state after the JIT probe (before the interpreter /// re-run) and, implicitly, is never needed for the interpreter's own /// post-run state since that's just left live for the final compare. - fn restore_into(&self, exec: &mut MipsExecutor, delay_slot_target: u64) { + fn restore_into(&self, exec: &mut MipsExecutor, delay_slot_target: u64) { exec.core.gpr = self.gpr; exec.core.hi = self.hi; exec.core.lo = self.lo; @@ -7637,7 +7633,7 @@ fn is_fusable_load_store(next_raw: u32, addr_reg: u8) -> bool { /// left holding the load/store's raw word permanently (not transient) — the /// fused handler needs it at execution time to decode the load/store's own /// rt/offset fields. -pub fn decode_into(ins: &mut DecodedInstr) { +pub fn decode_into(ins: &mut DecodedInstr) { let raw = ins.raw; let op = ((raw >> 26) & 0x3F) as u8; @@ -7653,10 +7649,7 @@ pub fn decode_into(ins: &mut DecodedInstr) { OP_SPECIAL => match funct as u32 { FUNCT_SLL => MipsExecutor::::exec_sll, // MOVCI is MIPS IV (MOVF/MOVT); an R4400 (MIPS III) must raise RI. - #[cfg(feature = "mips4")] - FUNCT_MOVCI => MipsExecutor::::exec_movci, - #[cfg(not(feature = "mips4"))] - FUNCT_MOVCI => MipsExecutor::::exec_reserved, + FUNCT_MOVCI => if C::MIPS4 { MipsExecutor::::exec_movci } else { MipsExecutor::::exec_reserved }, FUNCT_SRL => MipsExecutor::::exec_srl, FUNCT_SRA => MipsExecutor::::exec_sra, FUNCT_SLLV => MipsExecutor::::exec_sllv, @@ -7668,14 +7661,8 @@ pub fn decode_into(ins: &mut DecodedInstr) { FUNCT_JR => MipsExecutor::::exec_jr, FUNCT_JALR => MipsExecutor::::exec_jalr, // MOVZ/MOVN are MIPS IV; an R4400 (MIPS III) must raise RI. - #[cfg(feature = "mips4")] - FUNCT_MOVZ => MipsExecutor::::exec_movz, - #[cfg(feature = "mips4")] - FUNCT_MOVN => MipsExecutor::::exec_movn, - #[cfg(not(feature = "mips4"))] - FUNCT_MOVZ => MipsExecutor::::exec_reserved, - #[cfg(not(feature = "mips4"))] - FUNCT_MOVN => MipsExecutor::::exec_reserved, + FUNCT_MOVZ => if C::MIPS4 { MipsExecutor::::exec_movz } else { MipsExecutor::::exec_reserved }, + FUNCT_MOVN => if C::MIPS4 { MipsExecutor::::exec_movn } else { MipsExecutor::::exec_reserved }, FUNCT_SYSCALL => MipsExecutor::::exec_syscall, FUNCT_BREAK => MipsExecutor::::exec_break, FUNCT_SYNC => MipsExecutor::::exec_sync, @@ -7866,19 +7853,11 @@ pub fn decode_into(ins: &mut DecodedInstr) { FUNCT_FFLOOR_W => MipsExecutor::::exec_ffloor_w_s, // MOVF.fmt/MOVT.fmt/MOVZ.fmt/MOVN.fmt/RECIP.fmt/RSQRT.fmt are // MIPS IV; an R4400 (MIPS III) must raise RI for all five. - #[cfg(feature = "mips4")] - FUNCT_FMOVCF => MipsExecutor::::exec_fmovcf_s, - #[cfg(feature = "mips4")] - FUNCT_FMOVZ => MipsExecutor::::exec_fmovz_s, - #[cfg(feature = "mips4")] - FUNCT_FMOVN => MipsExecutor::::exec_fmovn_s, - #[cfg(feature = "mips4")] - FUNCT_FRECIP => MipsExecutor::::exec_frecip_s, - #[cfg(feature = "mips4")] - FUNCT_FRSQRT => MipsExecutor::::exec_frsqrt_s, - #[cfg(not(feature = "mips4"))] - FUNCT_FMOVCF | FUNCT_FMOVZ | FUNCT_FMOVN | FUNCT_FRECIP | FUNCT_FRSQRT - => MipsExecutor::::exec_reserved, + FUNCT_FMOVCF => if C::MIPS4 { MipsExecutor::::exec_fmovcf_s } else { MipsExecutor::::exec_reserved }, + FUNCT_FMOVZ => if C::MIPS4 { MipsExecutor::::exec_fmovz_s } else { MipsExecutor::::exec_reserved }, + FUNCT_FMOVN => if C::MIPS4 { MipsExecutor::::exec_fmovn_s } else { MipsExecutor::::exec_reserved }, + FUNCT_FRECIP => if C::MIPS4 { MipsExecutor::::exec_frecip_s } else { MipsExecutor::::exec_reserved }, + FUNCT_FRSQRT => if C::MIPS4 { MipsExecutor::::exec_frsqrt_s } else { MipsExecutor::::exec_reserved }, FUNCT_FCVT_D => MipsExecutor::::exec_fcvt_d_s, FUNCT_FCVT_W => MipsExecutor::::exec_fcvt_w_s, FUNCT_FCVT_L => MipsExecutor::::exec_fcvt_l_s, @@ -7904,19 +7883,11 @@ pub fn decode_into(ins: &mut DecodedInstr) { FUNCT_FFLOOR_W => MipsExecutor::::exec_ffloor_w_d, // MOVF.fmt/MOVT.fmt/MOVZ.fmt/MOVN.fmt/RECIP.fmt/RSQRT.fmt are // MIPS IV; an R4400 (MIPS III) must raise RI for all five. - #[cfg(feature = "mips4")] - FUNCT_FMOVCF => MipsExecutor::::exec_fmovcf_d, - #[cfg(feature = "mips4")] - FUNCT_FMOVZ => MipsExecutor::::exec_fmovz_d, - #[cfg(feature = "mips4")] - FUNCT_FMOVN => MipsExecutor::::exec_fmovn_d, - #[cfg(feature = "mips4")] - FUNCT_FRECIP => MipsExecutor::::exec_frecip_d, - #[cfg(feature = "mips4")] - FUNCT_FRSQRT => MipsExecutor::::exec_frsqrt_d, - #[cfg(not(feature = "mips4"))] - FUNCT_FMOVCF | FUNCT_FMOVZ | FUNCT_FMOVN | FUNCT_FRECIP | FUNCT_FRSQRT - => MipsExecutor::::exec_reserved, + FUNCT_FMOVCF => if C::MIPS4 { MipsExecutor::::exec_fmovcf_d } else { MipsExecutor::::exec_reserved }, + FUNCT_FMOVZ => if C::MIPS4 { MipsExecutor::::exec_fmovz_d } else { MipsExecutor::::exec_reserved }, + FUNCT_FMOVN => if C::MIPS4 { MipsExecutor::::exec_fmovn_d } else { MipsExecutor::::exec_reserved }, + FUNCT_FRECIP => if C::MIPS4 { MipsExecutor::::exec_frecip_d } else { MipsExecutor::::exec_reserved }, + FUNCT_FRSQRT => if C::MIPS4 { MipsExecutor::::exec_frsqrt_d } else { MipsExecutor::::exec_reserved }, FUNCT_FCVT_S => MipsExecutor::::exec_fcvt_s_d, FUNCT_FCVT_W => MipsExecutor::::exec_fcvt_w_d, FUNCT_FCVT_L => MipsExecutor::::exec_fcvt_l_d, @@ -7938,25 +7909,24 @@ pub fn decode_into(ins: &mut DecodedInstr) { // The entire COP1X opcode (LWXC1/LDXC1/SWXC1/SDXC1/PREFX and the // MADD/MSUB/NMADD/NMSUB family) is a MIPS IV addition — no MIPS III // encoding uses it, so an R4400 must raise RI for the whole opcode. - #[cfg(feature = "mips4")] - OP_COP1X => match funct as u32 { - FUNCT_LWXC1 => MipsExecutor::::exec_lwxc1, - FUNCT_LDXC1 => MipsExecutor::::exec_ldxc1, - FUNCT_SWXC1 => MipsExecutor::::exec_swxc1, - FUNCT_SDXC1 => MipsExecutor::::exec_sdxc1, - FUNCT_PREFX => MipsExecutor::::exec_prefx, - FUNCT_MADD_S => MipsExecutor::::exec_madd_s, - FUNCT_MADD_D => MipsExecutor::::exec_madd_d, - FUNCT_MSUB_S => MipsExecutor::::exec_msub_s, - FUNCT_MSUB_D => MipsExecutor::::exec_msub_d, - FUNCT_NMADD_S => MipsExecutor::::exec_nmadd_s, - FUNCT_NMADD_D => MipsExecutor::::exec_nmadd_d, - FUNCT_NMSUB_S => MipsExecutor::::exec_nmsub_s, - FUNCT_NMSUB_D => MipsExecutor::::exec_nmsub_d, - _ => MipsExecutor::::exec_reserved, - }, - #[cfg(not(feature = "mips4"))] - OP_COP1X => MipsExecutor::::exec_reserved, + OP_COP1X => if C::MIPS4 { + match funct as u32 { + FUNCT_LWXC1 => MipsExecutor::::exec_lwxc1, + FUNCT_LDXC1 => MipsExecutor::::exec_ldxc1, + FUNCT_SWXC1 => MipsExecutor::::exec_swxc1, + FUNCT_SDXC1 => MipsExecutor::::exec_sdxc1, + FUNCT_PREFX => MipsExecutor::::exec_prefx, + FUNCT_MADD_S => MipsExecutor::::exec_madd_s, + FUNCT_MADD_D => MipsExecutor::::exec_madd_d, + FUNCT_MSUB_S => MipsExecutor::::exec_msub_s, + FUNCT_MSUB_D => MipsExecutor::::exec_msub_d, + FUNCT_NMADD_S => MipsExecutor::::exec_nmadd_s, + FUNCT_NMADD_D => MipsExecutor::::exec_nmadd_d, + FUNCT_NMSUB_S => MipsExecutor::::exec_nmsub_s, + FUNCT_NMSUB_D => MipsExecutor::::exec_nmsub_d, + _ => MipsExecutor::::exec_reserved, + } + } else { MipsExecutor::::exec_reserved }, OP_LB => { ins.set_imm_se(raw); MipsExecutor::::exec_lb } OP_LH => { ins.set_imm_se(raw); MipsExecutor::::exec_lh } OP_LWL => { ins.set_imm_se(raw); MipsExecutor::::exec_lwl } @@ -7984,10 +7954,7 @@ pub fn decode_into(ins: &mut DecodedInstr) { OP_SDC1 => { ins.set_imm_se(raw); MipsExecutor::::exec_sdc1 } OP_SD => { ins.set_imm_se(raw); MipsExecutor::::exec_sd } // PREF is MIPS IV; an R4400 (MIPS III) must raise RI. - #[cfg(feature = "mips4")] - OP_PREF => MipsExecutor::::exec_pref, - #[cfg(not(feature = "mips4"))] - OP_PREF => MipsExecutor::::exec_reserved, + OP_PREF => if C::MIPS4 { MipsExecutor::::exec_pref } else { MipsExecutor::::exec_reserved }, OP_LLD => { ins.set_imm_se(raw); MipsExecutor::::exec_lld } OP_SCD => { ins.set_imm_se(raw); MipsExecutor::::exec_scd } _ => MipsExecutor::::exec_reserved, @@ -8044,7 +8011,7 @@ fn parse_reg_name(arg: &str) -> Option { /// Copy `data` to `vaddr` and zero-fill out to `memsz`. Writes go through the /// CPU's virtual-address path, so KSEG0/KSEG1 mapping, the MC address mask and /// bank remapping all apply — never poke `Memory` behind the bus. -fn load_range( +fn load_range( exec: &mut MipsExecutor, vaddr: u64, data: &[u8], @@ -8085,7 +8052,7 @@ fn load_range( /// The probe goes to the bus, not through `debug_write`: KSEG0 is cacheable, so /// a cached write is absorbed by L1D and reads back fine with nothing behind it. /// The original word is put back, so this leaves no trace. -fn probe_mapped(exec: &mut MipsExecutor, vaddr: u64) -> Result<(), String> { +fn probe_mapped(exec: &mut MipsExecutor, vaddr: u64) -> Result<(), String> { let tr = exec.debug_translate(vaddr); if tr.is_exception() { return Err(format!("{:#018x} does not translate (status {:#010x})", vaddr, tr.status)); @@ -8130,7 +8097,7 @@ fn probe_mapped(exec: &mut MipsExecutor, vaddr: u64) /// instruction can never execute. The v1 JIT needs no explicit flush (its /// CodeCache dies with the CPU thread, and loading requires a stopped CPU); /// jitv2 self-invalidates from the per-page generation counter the writes bump. -fn invalidate_loaded_range(exec: &mut MipsExecutor, vaddr: u64, len: u64) { +fn invalidate_loaded_range(exec: &mut MipsExecutor, vaddr: u64, len: u64) { let (_, iline) = exec.cache.get_config(CACH_PI); let (_, dline) = exec.cache.get_config(CACH_PD); let step = (iline.min(dline).max(4)) as u64; @@ -8677,7 +8644,7 @@ fn decode_cause(val: u32) -> String { } /// MipsCpu wrapper for threaded execution and monitor control -pub struct MipsCpu { +pub struct MipsCpu { executor: Arc>>, running: Arc, thread: Mutex>>, @@ -8705,10 +8672,10 @@ pub struct MipsCpu { // might read this pointer — the whole struct is already Send/Sync via its // other Arc fields; this one raw pointer needs the same guarantee spelled // out explicitly since raw pointers don't get it automatically. -unsafe impl Send for MipsCpu {} -unsafe impl Sync for MipsCpu {} +unsafe impl Send for MipsCpu {} +unsafe impl Sync for MipsCpu {} -impl MipsCpu { +impl MipsCpu { pub fn new(executor: MipsExecutor) -> Self { let fasttick_count = executor.core.fasttick_count.clone(); #[cfg(feature = "idle-pause")] @@ -9350,7 +9317,7 @@ fn is_call_instruction(instr: u32) -> bool { } } -impl Device for MipsCpu { +impl Device for MipsCpu { fn step(&self, cycles: u64) { let mut exec = self.executor.lock(); for _ in 0..cycles { @@ -11513,7 +11480,7 @@ impl Device for MipsCpu< } -impl MipsExecutor { +impl MipsExecutor { /// Dump the hottest sampled PCs and flag a likely idle-loop region: the /// smallest contiguous PC window (<=256 bytes) of always-interrupts-enabled /// samples that together account for the bulk of execution. @@ -11726,7 +11693,7 @@ impl MipsExecutor { // Resettable + Saveable for MipsCpu (CPU core + TLB) // ============================================================================ -impl Resettable for MipsCpu { +impl Resettable for MipsCpu { fn power_on(&self) { let mut exec = self.executor.lock(); exec.core.reset(false); @@ -11745,7 +11712,7 @@ impl Resettable for Mips } } -impl Saveable for MipsCpu { +impl Saveable for MipsCpu { fn save_state(&self) -> toml::Value { let exec = self.executor.lock(); let c = &exec.core; @@ -11904,14 +11871,14 @@ impl StopState { } /// Wraps `Arc>` to implement `CpuDebug`. -pub struct MipsCpuDebugAdapter { +pub struct MipsCpuDebugAdapter { cpu: Arc>, stop_state: Arc, // Allocator for GDB-owned breakpoint IDs (starts at 10000). next_gdb_bp_id: parking_lot::Mutex, } -impl MipsCpuDebugAdapter { +impl MipsCpuDebugAdapter { pub fn new(cpu: Arc>) -> Arc { Arc::new(Self { cpu, @@ -11921,7 +11888,7 @@ impl MipsCpuDebugAdapter } } -impl CpuDebug +impl CpuDebug for MipsCpuDebugAdapter { fn stop(&self) { diff --git a/src/mips_exec_test.rs b/src/mips_exec_test.rs index 54aa137..7893af4 100644 --- a/src/mips_exec_test.rs +++ b/src/mips_exec_test.rs @@ -6,7 +6,7 @@ mod tests { use crate::mips_exec::{MipsExecutor, MipsCpuConfig, DecodedInstr, EXEC_COMPLETE, EXEC_BREAKPOINT, EXEC_IS_EXCEPTION, EXEC_IS_TLB_REFILL, exec_exception, EXC_SYS, EXC_BP, EXC_TR, EXC_OV, EXC_RI, EXC_ADEL}; use crate::mips_isa::*; use crate::mips_tlb::PassthroughTlb; - use crate::mips_cache_v2::{PassthroughCache, MipsCache, R4000Cache}; + use crate::mips_cache_v2::{PassthroughCache, PassthroughCacheM4, MipsCache, R4400Cache, CpuModel}; use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BusDevice}; use std::sync::Arc; @@ -147,14 +147,14 @@ mod tests { /// see `MockMemory::gen_ptr`'s doc comment for why this, not a null /// `gen_ptr`, is what actually keeps jitv2 out of this file's tests now. #[cfg(feature = "jitv2")] - fn disable_jitv2(exec: &mut MipsExecutor) { + fn disable_jitv2(exec: &mut MipsExecutor) { // jitv2_dispatch_enabled=false turns the whole JIT dispatch gate off, // which is what drives lockstep now — so this alone keeps jitv2 (and its // lockstep instrumentation) out of these interpreter-only tests. exec.jitv2_dispatch_enabled = false; } #[cfg(not(feature = "jitv2"))] - fn disable_jitv2(_exec: &mut MipsExecutor) {} + fn disable_jitv2(_exec: &mut MipsExecutor) {} // Helper to create executor with mock memory fn create_executor() -> (MipsExecutor, Arc) { @@ -166,6 +166,15 @@ mod tests { (exec, mem) } + fn create_executor_m4() -> (MipsExecutor, Arc) { + let mem = Arc::new(MockMemory::new()); + let mem_bus: Arc = mem.clone(); + let cfg = MipsCpuConfig::indy(); + let mut exec = MipsExecutor::new(mem_bus, PassthroughTlb::default(), &cfg); + disable_jitv2(&mut exec); + (exec, mem) + } + // Helper to create executor with specific TLB fn create_executor_with_tlb(tlb: T) -> (MipsExecutor, Arc) { let mem = Arc::new(MockMemory::new()); @@ -176,8 +185,8 @@ mod tests { (exec, mem) } - // Helper to create executor with R4000Cache for VCE testing - fn create_executor_with_r4000cache() -> (MipsExecutor, Arc) { + // Helper to create executor with R4400Cache for VCE testing + fn create_executor_with_r4000cache() -> (MipsExecutor, Arc) { let mem = Arc::new(MockMemory::new()); let mem_bus: Arc = mem.clone(); let cfg = MipsCpuConfig::indy(); @@ -2400,9 +2409,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_conditional_moves() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); // MOVZ r3, r1, r2 (Move r1 to r3 if r2 == 0) exec.core.write_gpr(1, 0xDEADBEEF); @@ -2557,9 +2565,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_pref() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.write_gpr(1, 0x1000); // PREF 0, 0(r1) let instr_pref = make_i(OP_PREF, 1, 0, 0); @@ -2766,9 +2773,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_fpu_mov_cond() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -2800,9 +2806,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_fpu_recip_rsqrt() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -2820,9 +2825,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_cop1x_load_store() { - let (mut exec, mem) = create_executor(); + let (mut exec, mem) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -2876,9 +2880,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_cop1x_madd() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -2909,9 +2912,8 @@ mod tests { // Covers the remaining MIPS IV COP1X fused ops not exercised by test_cop1x_madd // (MADD.D, MSUB.S, NMADD.S/D, NMSUB.S/D) — fd = fs*ft +/- fr, negated for NMADD/NMSUB. #[test] - #[cfg(feature = "mips4")] fn test_cop1x_madd_remaining_variants() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -2967,9 +2969,8 @@ mod tests { // Covers SDXC1 (missing from test_cop1x_load_store, which only checks LWXC1/SWXC1/LDXC1) // and PREFX (COP1X prefetch — architecturally a hint/no-op). #[test] - #[cfg(feature = "mips4")] fn test_cop1x_sdxc1_and_prefx() { - let (mut exec, mem) = create_executor(); + let (mut exec, mem) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -3020,9 +3021,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_movci() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -3075,9 +3075,8 @@ mod tests { } #[test] - #[cfg(feature = "mips4")] fn test_fpu_movcf() { - let (mut exec, _) = create_executor(); + let (mut exec, _) = create_executor_m4(); exec.core.cp0_status |= STATUS_CU1 | STATUS_FR; exec.update_fpr_mode(); @@ -3332,7 +3331,7 @@ mod tests { #[test] #[cfg(feature = "jitv2")] fn test_r4000cache_step_sequence() { - // Exercise the full R4000Cache path: kseg0 fetch → L2 fill → L1I fill → exec_decoded. + // Exercise the full R4400Cache path: kseg0 fetch → L2 fill → L1I fill → exec_decoded. // PC 0x80000000 → phys 0x00000000 (kseg0, cacheable). Uses // JitCapableMockMemory (not the shared MockMemory) plus // install_jit_hooks specifically so jitv2 dispatch is exercisable @@ -3340,7 +3339,7 @@ mod tests { let mem = Arc::new(JitCapableMockMemory::new()); let mem_bus: Arc = mem.clone(); let cfg = MipsCpuConfig::indy(); - let mut exec: MipsExecutor = MipsExecutor::new(mem_bus, PassthroughTlb::default(), &cfg); + let mut exec: MipsExecutor = MipsExecutor::new(mem_bus, PassthroughTlb::default(), &cfg); exec.install_jit_hooks(); // Write instructions into memory as big-endian u32s packed into u64 chunks. @@ -3724,7 +3723,7 @@ mod tests { use crate::mips_exec::{MipsExecutor, MipsCpuConfig, EXC_VCED, EXC_VCEI}; use crate::mips_tlb::PassthroughTlb; #[allow(unused_imports)] - use crate::mips_cache_v2::R4000Cache; + use crate::mips_cache_v2::R4400Cache; #[allow(unused_imports)] use crate::mips_core::STATUS_KX; use crate::traits::{BUS_OK, BUS_VCE}; From 9ef7b06406b924e74d5f6b337a08ea7cd542b16a Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 21:46:25 -0400 Subject: [PATCH 2/9] machine: select the CPU at runtime, not at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[machine] cpu = "r4400" | "r5000"` picks the model from config, beside the existing `profile`. One binary now runs either CPU: same default build, no cargo feature cpu = "r4400" -> guest reports R4400, 47.76 MIPS, 40/40 cpu = "r5000" -> guest reports R5000, 42.19 MIPS, 40/40 which matches what the per-cell compile-time builds measured (~50 / ~43). Machine holds `Arc` and chooses the monomorphisation once, at construction: let cpu: Arc = match cfg_cpu_model { CpuModel::R4400 => build_cpu!(R4400Cache), CpuModel::R5000 => build_cpu!(R5000Cache), }; The CPU thread runs inside the chosen type, so the vtable is only ever on setup, control and debug calls — never on the instruction path. CpuDevice is 22 such methods; the two awkward ones were the L1-I counters (now a MipsCache::set_l1i_counters default method rather than a public field poke) and the gdb adapter, which needs `self: Arc` to erase. Fixes a real ordering bug found by running it rather than by the suite: MipsCore::new calls reset_registers before the model is known, so setting only `reset_prid`/`reset_fir` left the live CP0 PRId and CP1 FIR at their R4400 defaults. An R5000 machine had R5000 cache geometry — visible in the throughput split — but told the guest it was an R4400. The constructor now sets both the reset value and the live register. cpu_model_identity_is_live_at_construction covers it; verified failing with the fix reverted. Tests 419 -> 420 default, 418 -> 419 with r5k. The r5k cargo feature is now vestigial for model selection and can be retired separately. Co-Authored-By: Claude Opus 5 (1M context) --- src/config.rs | 23 ++++++++++++++- src/machine.rs | 42 ++++++++++++++------------ src/mips_cache_v2.rs | 8 +++++ src/mips_exec.rs | 68 +++++++++++++++++++++++++++++++++++++++++++ src/mips_exec_test.rs | 29 ++++++++++++++++++ 5 files changed, 150 insertions(+), 20 deletions(-) diff --git a/src/config.rs b/src/config.rs index b7eca5e..d9ca79b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -510,17 +510,38 @@ impl Default for GraphicsSection { } } +/// Emulated CPU. Runtime-selectable: each model is its own monomorphisation, +/// so the hot path carries no per-model branch. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum CpuModel { + /// MIPS R4400, 16K direct-mapped L1s, 1 MB L2. The Indy IRIS has always shipped. + #[default] + R4400, + /// MIPS R5000, 2-way 32K L1s, no secondary cache, MIPS IV. + R5000, +} + +impl CpuModel { + pub const ALL: [Self; 2] = [Self::R4400, Self::R5000]; + pub fn label(self) -> &'static str { + match self { Self::R4400 => "MIPS R4400", Self::R5000 => "MIPS R5000" } + } +} + /// `[machine]` section — platform identity (not performance knobs). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MachineSection { #[serde(default)] pub profile: MachineProfile, + #[serde(default)] + pub cpu: CpuModel, } impl Default for MachineSection { fn default() -> Self { - Self { profile: MachineProfile::default() } + Self { profile: MachineProfile::default(), cpu: CpuModel::default() } } } diff --git a/src/machine.rs b/src/machine.rs index f4278f7..b65acfa 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -26,11 +26,7 @@ use crate::mc::MemoryController; use crate::mips_tlb::MipsTlb; use crate::mips_exec::{MipsExecutor, MipsCpu, MipsCpuConfig, MipsCpuDebugAdapter}; use crate::gdb_stub::CpuDebug; -// Step 1 keeps the cargo feature as the selector; step 3 makes this a runtime choice. -#[cfg(not(feature = "r5k"))] -use crate::mips_cache_v2::R4400Cache as SelectedCache; -#[cfg(feature = "r5k")] -use crate::mips_cache_v2::R5000Cache as SelectedCache; +use crate::mips_cache_v2::{MipsCache, R4400Cache, R5000Cache}; use crate::hpc3::Hpc3; use crate::ioc::{Ioc, GioSlot, GIO_SLOT_MAP, profile_idx}; use crate::monitor::Monitor; @@ -64,7 +60,7 @@ pub fn emulator_name() -> &'static str { } pub struct Machine { - cpu: Arc>, + cpu: Arc, _phys: Arc, // Keep reference to Physical Bus mc: MemoryController, hpc3: Hpc3, @@ -252,6 +248,7 @@ impl Machine { let display_resolution = cfg.graphics.resolution; let newport_active = !cfg.headless && cfg.graphics.board == GraphicsBoard::Newport; let clock_fixed_mhz = cfg.clock.fixed_mhz; + let cfg_cpu_model = cfg.machine.cpu; if !cfg.machine.profile.supported() { eprintln!( @@ -699,11 +696,13 @@ impl Machine { phys.vino.start(); } - // 5. CPU config + TLB + Executor + // 5. CPU config + TLB + Executor. The model is a runtime choice, but each + // arm below monomorphises its own CPU — no per-model branch on the hot path. + let sysad: Arc = phys.clone(); + macro_rules! build_cpu { ($cache:ty) => {{ let cfg = MipsCpuConfig::indy(); let tlb = MipsTlb::new(cfg.tlb_entries); - let sysad: Arc = phys.clone(); - let mut executor: MipsExecutor = MipsExecutor::new(sysad, tlb, &cfg); + let mut executor: MipsExecutor = MipsExecutor::new(sysad.clone(), tlb, &cfg); // Load default symbol maps if they exist { @@ -729,15 +728,10 @@ impl Machine { executor.core.fasttick_count = fasttick_count; executor.decoded_count = decoded_count; executor.uncached_fetch_count = Arc::clone(&uncached_fetch_count); - executor.cache.l1i_hit_count = Arc::clone(&l1i_hit_count); - executor.cache.l1i_fetch_count = Arc::clone(&l1i_fetch_count); + executor.cache.set_l1i_counters(Arc::clone(&l1i_hit_count), Arc::clone(&l1i_fetch_count)); // Re-sync raw pointers after Arc injection (the Arcs above replaced the ones captured in new()). executor.rebind_atomic_ptrs(); - // Share count_hz_atomic from MipsCore with Rex3 so the refresh thread can display it. - #[cfg(feature = "developer")] - if let Some(rex3) = &phys.rex3 { rex3.set_count_hz_atomic(Arc::clone(&executor.core.count_hz_atomic)); } - // Give the core the machine's hptimer manager: CP0 Compare writes // arm a one-shot on it that raises IP7 from the timer thread. Safe // to hand over before the move into MipsCpu below — nothing gets @@ -746,10 +740,20 @@ impl Machine { // inside the executor's Arc>. executor.core.set_timer_manager(timer_manager.clone()); - let cpu = Arc::new(MipsCpu::new(executor)); + Arc::new(MipsCpu::new(executor)) as Arc + }}} + + let cpu: Arc = match cfg_cpu_model { + crate::config::CpuModel::R4400 => build_cpu!(R4400Cache), + crate::config::CpuModel::R5000 => build_cpu!(R5000Cache), + }; + + // Share count_hz_atomic from MipsCore with Rex3 so the refresh thread can display it. + #[cfg(feature = "developer")] + if let Some(rex3) = &phys.rex3 { rex3.set_count_hz_atomic(cpu.count_hz_atomic()); } // Connect CPU to MC and IOC for signaling - let cpu_device: Arc = cpu.clone(); + let cpu_device: Arc = cpu.clone().as_device(); mc.set_cpu(Arc::downgrade(&cpu_device)); ioc.set_interrupts(cpu.interrupts_ptr()); @@ -837,7 +841,7 @@ impl Machine { monitor.register_device(crate::perf_monitor::PerfMonitor::new( cpu.running_flag(), cpu.cycles_ptr(), - cpu.fasttick_count.clone(), + cpu.fasttick_count(), phys.rex3.clone(), hpc3.hal2().cloned(), )); @@ -1092,7 +1096,7 @@ impl Machine { /// Return a type-erased CpuDebug handle for the GDB stub. pub fn get_cpu_debug(&self) -> Arc { - MipsCpuDebugAdapter::new(self.cpu.clone()) + self.cpu.clone().debug_adapter() } /// Load a static ELF32 MSB binary into RAM and set PC to its entry point diff --git a/src/mips_cache_v2.rs b/src/mips_cache_v2.rs index b682a2c..1deb91e 100644 --- a/src/mips_cache_v2.rs +++ b/src/mips_cache_v2.rs @@ -344,6 +344,9 @@ pub trait CpuModel: MipsCache { } pub trait MipsCache: Send + Sync { + /// Share the L1-I hit/fetch counters with the status display. No-op where absent. + fn set_l1i_counters(&mut self, _hit: Arc, _fetch: Arc) {} + /// L1/L2 geometry, so CP0 Config reports this model rather than a build-time constant. const IC_SIZE: usize; const IC_LINE: usize; @@ -2042,6 +2045,11 @@ impl MipsCache for CpuCache { + fn set_l1i_counters(&mut self, hit: Arc, fetch: Arc) { + self.l1i_hit_count = hit; + self.l1i_fetch_count = fetch; + } + const IC_SIZE: usize = IC_SIZE; const IC_LINE: usize = IC_LINE; const IC_WAYS: usize = IC_WAYS; diff --git a/src/mips_exec.rs b/src/mips_exec.rs index ddba6aa..14168a3 100644 --- a/src/mips_exec.rs +++ b/src/mips_exec.rs @@ -1950,8 +1950,13 @@ impl MipsExecutor { core.cp0_config = config; core.tlb_entries = C::TLB_ENTRIES as u32; + // MipsCore::new already ran reset_registers, so set both the reset value + // and the live register — otherwise the model's identity only appears + // after the next power_on. core.reset_prid = C::PRID; core.reset_fir = C::FIR; + core.cp0_prid = C::PRID; + core.fpu_fir = C::FIR; // Triton: sync initial L2 enabled state from Config SE bit (starts 0 = disabled). #[cfg(feature = "r5ksc_triton")] @@ -11693,6 +11698,69 @@ impl MipsExecutor { // Resettable + Saveable for MipsCpu (CPU core + TLB) // ============================================================================ +/// Type-erased CPU handle. The CPU thread runs inside the implementor, so every +/// call here is setup, control or debug — never the instruction hot path. +pub trait CpuDevice: Device + Resettable + Saveable + Send + Sync { + fn as_device(self: Arc) -> Arc; + fn debug_adapter(self: Arc) -> Arc; + fn cycles_ptr(&self) -> crate::mips_core::CyclesPtr; + fn interrupts_ptr(&self) -> *const AtomicU64; + fn core_ptr(&self) -> *const crate::mips_core::MipsCore; + fn register_locks(&self); + fn load_elf(&self, path: &str) -> Result; + fn load_elf_bytes(&self, bytes: &[u8], name: &str) -> Result; + fn step_n_inline(&self, n: u64) -> Result; + fn step_one_inline_counting_instructions(&self) -> Result; + fn state_digest(&self) -> Result; + fn restore_state_digest(&self, d: &CpuStateDigest) -> Result<(), String>; + fn fixup_cp0_count(&self, d: &CpuStateDigest) -> Result<(), String>; + fn set_jitv2_dispatch_enabled(&self, on: bool) -> Result; + fn set_hw_read_fixup_recording(&self, on: bool) -> Result<(), String>; + fn set_hw_read_fixup_replay(&self, f: Option>) -> Result<(), String>; + /// CPU model name, as the guest and the benchmark report see it. + fn model_name(&self) -> &'static str; + fn set_cheritest_dump_hook(&self, on: bool); + fn running_flag(&self) -> Arc; + fn fasttick_count(&self) -> Arc; + fn count_hz_atomic(&self) -> Arc; + #[cfg(feature = "jitv2")] + fn jitv2(&self) -> Arc>; +} + +impl CpuDevice for MipsCpu { + fn as_device(self: Arc) -> Arc { self } + fn debug_adapter(self: Arc) -> Arc { + MipsCpuDebugAdapter::new(self) + } + fn cycles_ptr(&self) -> crate::mips_core::CyclesPtr { MipsCpu::cycles_ptr(self) } + fn interrupts_ptr(&self) -> *const AtomicU64 { MipsCpu::interrupts_ptr(self) } + fn core_ptr(&self) -> *const crate::mips_core::MipsCore { MipsCpu::core_ptr(self) } + fn register_locks(&self) { MipsCpu::register_locks(self) } + fn load_elf(&self, p: &str) -> Result { MipsCpu::load_elf(self, p) } + fn load_elf_bytes(&self, b: &[u8], n: &str) -> Result { MipsCpu::load_elf_bytes(self, b, n) } + fn step_n_inline(&self, n: u64) -> Result { MipsCpu::step_n_inline(self, n) } + fn step_one_inline_counting_instructions(&self) -> Result { + MipsCpu::step_one_inline_counting_instructions(self) + } + fn state_digest(&self) -> Result { MipsCpu::state_digest(self) } + fn restore_state_digest(&self, d: &CpuStateDigest) -> Result<(), String> { MipsCpu::restore_state_digest(self, d) } + fn fixup_cp0_count(&self, d: &CpuStateDigest) -> Result<(), String> { MipsCpu::fixup_cp0_count(self, d) } + fn set_jitv2_dispatch_enabled(&self, on: bool) -> Result { MipsCpu::set_jitv2_dispatch_enabled(self, on) } + fn set_hw_read_fixup_recording(&self, on: bool) -> Result<(), String> { MipsCpu::set_hw_read_fixup_recording(self, on) } + fn set_hw_read_fixup_replay(&self, f: Option>) -> Result<(), String> { + MipsCpu::set_hw_read_fixup_replay(self, f) + } + fn model_name(&self) -> &'static str { C::NAME } + fn set_cheritest_dump_hook(&self, on: bool) { MipsCpu::set_cheritest_dump_hook(self, on) } + fn running_flag(&self) -> Arc { MipsCpu::running_flag(self) } + fn fasttick_count(&self) -> Arc { Arc::clone(&self.fasttick_count) } + fn count_hz_atomic(&self) -> Arc { + Arc::clone(&self.executor.lock().core.count_hz_atomic) + } + #[cfg(feature = "jitv2")] + fn jitv2(&self) -> Arc> { MipsCpu::jitv2(self) } +} + impl Resettable for MipsCpu { fn power_on(&self) { let mut exec = self.executor.lock(); diff --git a/src/mips_exec_test.rs b/src/mips_exec_test.rs index 7893af4..a38588c 100644 --- a/src/mips_exec_test.rs +++ b/src/mips_exec_test.rs @@ -195,6 +195,35 @@ mod tests { (exec, mem) } + /// A model's identity must be live the moment the executor exists — MipsCore::new + /// runs reset_registers before the model is known, so the constructor has to set + /// both the reset value and the live register. Caught a real bug: the guest read + /// R4400 out of an R5000 machine because only the reset value had been updated. + #[test] + fn cpu_model_identity_is_live_at_construction() { + fn build>>() + -> MipsExecutor { + let mem: Arc = Arc::new(MockMemory::new()); + MipsExecutor::new(mem, PassthroughTlb::default(), &MipsCpuConfig::indy()) + } + let e4 = build::(); + assert_eq!(e4.core.cp0_prid, 0x0000_0440, "R4400 PRId at construction"); + assert_eq!(e4.core.fpu_fir, 0x0000_0500, "R4400 FIR at construction"); + assert!(!R4400Cache::MIPS4, "R4400 is MIPS III"); + + let e5 = build::(); + assert_eq!(e5.core.cp0_prid, 0x0000_2321, "R5000 PRId at construction"); + assert_eq!(e5.core.fpu_fir, 0x0000_2300, "R5000 FIR at construction"); + assert!(crate::mips_cache_v2::R5000Cache::MIPS4, "R5000 is MIPS IV"); + + // and they must survive a power_on, which re-derives from the reset values + let e5b = build::(); + let mut core = e5b.core; + core.reset(false); + assert_eq!(core.cp0_prid, 0x0000_2321, "R5000 PRId after reset"); + assert_eq!(core.fpu_fir, 0x0000_2300, "R5000 FIR after reset"); + } + // Instruction builders fn make_r(op: u32, rs: u32, rt: u32, rd: u32, sa: u32, funct: u32) -> u32 { (op << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | ((rd & 0x1F) << 11) | ((sa & 0x1F) << 6) | (funct & 0x3F) From 4989f9a251affdee9e33125c06d5548b2d090203 Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 22:14:18 -0400 Subject: [PATCH 3/9] snapshot: record the CPU model and refuse a crossed restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CPU became a runtime choice in the previous commit, which turned a Cargo-flag footgun into a two-clicks-in-the-GUI one: an R4400 snapshot restored onto an R5000 machine would quietly proceed, even though PRId, FIR and the whole cache geometry differ and the captured state assumes the model it was taken on. The provenance framework was already there — build features and disk identity are hard errors on restore, paths and nvram are warnings, and IRIS_SNAPSHOT_SKIP_CHECK=1 downgrades the lot. Two things were missing. `cpu_model` is a new manifest field, because the model is config now and so is not covered by the feature list at all. Absent means a legacy manifest, written when the model really was a build flag, and those still restore — there is nothing recorded to compare against. `enabled_features()` also omitted jitv2, jitv2_opcodefusion, opcodefusion and idle-pause, all of which change execution semantics the captured state depends on. They are recorded now. A snapshot taken by a jitv2 build before this commit recorded an incomplete list and will be flagged against a jitv2 build after it; that is the check doing its job, and the existing skip env var covers anyone who needs the old one back. The comparison is a free function rather than eight lines inline in Machine::load_snapshot, so the decision is unit-testable without standing up a machine. Verified end to end on a real emulator as well: save on cpu = "r4400" -> manifest carries cpu_model = "R4400" load on cpu = "r5000" -> Error: snapshot provenance mismatch: - CPU model differs: snapshot R4400 vs current R5000 — set [machine] cpu = "r4400" load on cpu = "r4400" -> Snapshot loaded from saves/cputest Tests 420 -> 422. Co-Authored-By: Claude Opus 5 (1M context) --- src/machine.rs | 6 +++++ src/snapshot.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/machine.rs b/src/machine.rs index b65acfa..5f6abc7 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1627,6 +1627,8 @@ impl Machine { // filled by for_current_save.) manifest.disks = self.disks.clone(); manifest.nvram = Some(self.nvram_path.clone()); + // The CPU is a runtime choice now, so it is state the restore must match. + manifest.cpu_model = Some(self.cpu.model_name().to_string()); snap.write_manifest(&manifest).map_err(|e| e.to_string())?; let sv = manifest.schema_version; @@ -1818,6 +1820,10 @@ impl Machine { m.features.join(","), cur_features.join(",") )); } + // The CPU model is guest-visible (PRId/FIR, cache geometry) and + // no longer a build flag, so a mismatch is as fatal as a feature one. + if let Some(e) = crate::snapshot::cpu_model_mismatch( + m.cpu_model.as_deref(), self.cpu.model_name()) { fatal.push(e); } // Every recorded disk must still be configured at the same SCSI // id with the same size. The host *path* is only where the file // happens to live (it moves when disks are relocated, e.g. into a diff --git a/src/snapshot.rs b/src/snapshot.rs index 704cf5a..b9d57c0 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -61,6 +61,19 @@ pub struct DiskRef { pub size_bytes: u64, } +/// Reject a restore whose CPU model differs from the running machine's. +/// `None` from the snapshot means a legacy manifest — the model was a build +/// flag then, so there is nothing to compare and the restore is allowed. +pub fn cpu_model_mismatch(snapshot: Option<&str>, current: &str) -> Option { + match snapshot { + Some(sn) if sn != current => Some(format!( + "CPU model differs: snapshot {} vs current {} — set [machine] cpu = \"{}\"", + sn, current, sn.to_lowercase() + )), + _ => None, + } +} + /// Build the list of cargo features enabled in this binary. Recorded in the /// manifest and required to match on restore, since features such as `ci_clock` /// (synthetic clock) change CPU/timer semantics that the captured @@ -70,6 +83,10 @@ pub fn enabled_features() -> Vec { macro_rules! push_if { ($name:literal) => { if cfg!(feature = $name) { f.push($name.to_string()); } } } push_if!("rex-jit"); push_if!("lightning"); + push_if!("jitv2"); + push_if!("jitv2_opcodefusion"); + push_if!("opcodefusion"); + push_if!("idle-pause"); push_if!("ci_clock"); push_if!("tlbvmap"); push_if!("chd"); @@ -103,6 +120,9 @@ pub struct Manifest { pub disks: Vec, /// Configured nvram file path at capture time. None for legacy manifests. pub nvram: Option, + /// Emulated CPU at capture time. None for legacy manifests, and for those + /// the check is skipped — the model was a build flag before it was config. + pub cpu_model: Option, } impl Manifest { @@ -126,6 +146,7 @@ impl Manifest { // configured disks and nvram path. disks: Vec::new(), nvram: None, + cpu_model: None, } } @@ -162,6 +183,9 @@ impl Manifest { }).collect(); tbl.insert("disks".into(), Value::Array(disks)); } + if let Some(cm) = &self.cpu_model { + tbl.insert("cpu_model".into(), Value::String(cm.clone())); + } if let Some(nv) = &self.nvram { tbl.insert("nvram".into(), Value::String(nv.clone())); } @@ -206,6 +230,7 @@ impl Manifest { }).collect()) .unwrap_or_default(); let nvram = tbl.get("nvram").and_then(|x| x.as_str()).map(String::from); + let cpu_model = tbl.get("cpu_model").and_then(|x| x.as_str()).map(String::from); Ok(Self { schema_version, iris_git_rev, @@ -217,6 +242,7 @@ impl Manifest { features, disks, nvram, + cpu_model, }) } } @@ -585,6 +611,7 @@ mod tests { features: vec!["jit".into(), "tlbvmap".into()], disks: vec![DiskRef { id: 1, path: "irix53.raw".into(), size_bytes: 4294967296 }], nvram: Some("nvram-irix53.bin".into()), + cpu_model: Some("R5000".into()), }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -598,6 +625,7 @@ mod tests { assert_eq!(m2.features, m.features); assert_eq!(m2.disks, m.disks); assert_eq!(m2.nvram, m.nvram); + assert_eq!(m2.cpu_model, m.cpu_model); } #[test] @@ -613,6 +641,7 @@ mod tests { features: vec![], disks: vec![], nvram: None, + cpu_model: None, }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -623,6 +652,47 @@ mod tests { assert!(m2.features.is_empty()); assert!(m2.disks.is_empty()); assert!(m2.nvram.is_none()); + assert!(m2.cpu_model.is_none(), "legacy manifest has no cpu_model"); + } + + #[test] + fn a_snapshot_only_restores_onto_the_cpu_it_was_taken_on() { + // same model: fine + assert!(cpu_model_mismatch(Some("R4400"), "R4400").is_none()); + assert!(cpu_model_mismatch(Some("R5000"), "R5000").is_none()); + // legacy manifest: nothing recorded, so nothing to refuse + assert!(cpu_model_mismatch(None, "R5000").is_none()); + // crossed models: refused, and the message says how to fix it + let e = cpu_model_mismatch(Some("R4400"), "R5000").expect("must refuse"); + assert!(e.contains("R4400") && e.contains("R5000"), "{}", e); + assert!(e.contains(r#"cpu = "r4400""#), "must name the config fix: {}", e); + assert!(cpu_model_mismatch(Some("R5000"), "R4400").is_some()); + } + + /// The CPU model must survive a manifest round trip, and a legacy manifest + /// (written before the model was config) must still parse as "unknown" so + /// old snapshots keep loading instead of being refused outright. + #[test] + fn cpu_model_round_trips_and_legacy_stays_unknown() { + let mut m = Manifest::for_current_save(); + m.cpu_model = Some("R5000".into()); + let back = Manifest::from_toml(&m.to_toml()).expect("round trip"); + assert_eq!(back.cpu_model.as_deref(), Some("R5000")); + + // a manifest with no cpu_model key at all + let mut t = m.to_toml(); + t.as_table_mut().unwrap().remove("cpu_model"); + let legacy = Manifest::from_toml(&t).expect("legacy parses"); + assert!(legacy.cpu_model.is_none(), "absent cpu_model must read as unknown, not error"); + + // and the execution-affecting flags are now recorded + let f = enabled_features(); + for name in ["jitv2", "opcodefusion", "idle-pause"] { + assert_eq!(f.contains(&name.to_string()), cfg!(feature = "jitv2") && name == "jitv2" + || cfg!(feature = "opcodefusion") && name == "opcodefusion" + || cfg!(feature = "idle-pause") && name == "idle-pause", + "{} recorded iff enabled", name); + } } #[test] From 85e44a4635dbb02bb76bd3f58963aba5ec03ef4c Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 22:54:23 -0400 Subject: [PATCH 4/9] iris-gui: let the user pick the CPU, because it is a setting now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Memory tab showed a read-only build constant and said the CPU "is fixed at build time ... To use a different CPU, download that build." That stopped being true at 96e5ddd: both cache models are monomorphised into every binary and Machine::new picks between them from cfg.machine.cpu. The config field, its label() and its ALL list were all already there — nothing surfaced them. So the Memory tab gets a combo box, gated on the machine being stopped and showing running-vs-pending the way the RAM controls already do, and the helper text now describes what actually differs: 16 KB direct-mapped and MIPS III against 2-way 32 KB, no secondary cache and MIPS IV. IRIX reads PRId and configures itself from it, so this is a different machine to the guest rather than a speed knob. Verified end to end rather than by reading the code, using the machine inventory the benchmark suite now prints — one binary, config alone: default R4400 rev 4.0 PRId 0x00000440 16 KB/16 B L2 present cpu = r5000 R5000 rev 2.1 PRId 0x00002321 32 KB/32 B L2 absent **And a bug that fell out of it.** Nothing but the config selects the model, so `--features r5k` no longer produces an R5000 — but that is exactly how both CI matrices and cpu-tests/run/matrix.sh were still choosing one. Their r5000 cells were building a separate binary to get a machine identical to the r4400 cell's, and their own guards ("ran cpu=R4400, expected cpu=R5000") would have caught it on the next push. 96e5ddd changed no CI file and no run config; this does. Adds `--cpu r4400|r5000` so a runner can pick without a second config file, and uses it in suites.yml (both suites) and matrix.sh. Checked that iris-bench forwards it: `run --iris … -- --cpu r5000` produces an R5000 guest at 40/40. Also removes build_features::CPU. It had no callers left once the Memory tab stopped using it, and a constant derived from cargo features can only report how the binary was compiled — which is no longer the same question as which CPU is running, and it was being shown to users as though it were. The r5k cargo feature stays for now: it is what the r5ksc guards key off, and retiring it properly means deciding whether the CI matrix still needs separate r4400/r5000 emulator builds at all. It probably does not. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/suites.yml | 10 ++++++-- cpu-tests/run/matrix.sh | 4 +++ iris-gui/Cargo.toml | 7 ++--- iris-gui/src/config_ui.rs | 50 ++++++++++++++++++++++++++++++------ iris-gui/src/main.rs | 5 ++++ src/config.rs | 13 +++++++++- src/lib.rs | 21 ++++++--------- 7 files changed, 83 insertions(+), 27 deletions(-) diff --git a/.github/workflows/suites.yml b/.github/workflows/suites.yml index 288d3fc..4d3e6cc 100644 --- a/.github/workflows/suites.yml +++ b/.github/workflows/suites.yml @@ -170,8 +170,12 @@ jobs: - uses: Swatinem/rust-cache@v2 - # The CPU and the JIT are compile-time cargo features, not runtime - # switches — see rules/perf/hardware-profiles.md. + # The JIT is a compile-time cargo feature. The CPU is *not*, any more: + # 96e5ddd made both cache models monomorphisations selected at machine + # construction, so `--features r5k` no longer picks the R5000 and the run + # steps below pass `--cpu` instead. The feature is still set here only + # because it is what the r5ksc guards key off; it is vestigial for model + # selection and can be retired separately. # # Plain r5k only: r5ksc (the external R4600SC-style secondary cache a real # Indy R5000 board has) and r5ksc_triton (the O2's on-die L2, not a machine @@ -214,6 +218,7 @@ jobs: chmod +x build/cputest.elf timeout 900 ../target/release/iris \ --config run/bare.toml \ + --cpu ${{ matrix.cpu }} \ --load-elf build/cputest.elf \ --test-device --test-device-dump build/dump.json \ --headless --noaudio 2>&1 | tee build/run.log @@ -244,6 +249,7 @@ jobs: --iris ./target/release/iris \ --label "${{ matrix.cpu }}-${{ matrix.engine }}" \ --timeout 2400 \ + -- --cpu ${{ matrix.cpu }} \ 2>&1 | tee bench/build/run.log # Three ways this can fail quietly, so all three are checked. A build diff --git a/cpu-tests/run/matrix.sh b/cpu-tests/run/matrix.sh index 2b7af90..422bd6e 100755 --- a/cpu-tests/run/matrix.sh +++ b/cpu-tests/run/matrix.sh @@ -71,8 +71,12 @@ run_cell() { build_iris "$cpu" "$engine" || { echo "FAIL $cell (build)"; return 1; } + # --cpu, not the r5k cargo feature: since 96e5ddd both cache models are + # compiled into every build and the model is chosen at construction, so + # building with --features r5k no longer produces an R5000 machine. timeout "${TIMEOUT:-1200}" "$iris" \ --config run/bare.toml \ + --cpu "$cpu" \ --load-elf build/cputest.elf \ --test-device --test-device-dump "$OUT/$cell.dump.json" \ --headless --noaudio > "$log" 2>&1 diff --git a/iris-gui/Cargo.toml b/iris-gui/Cargo.toml index 5955c32..dbf4711 100644 --- a/iris-gui/Cargo.toml +++ b/iris-gui/Cargo.toml @@ -76,9 +76,10 @@ pcap = ["iris/pcap"] # "rebuild with --features daynaport" warning, so a config stays editable. # Build with: cargo build -p iris-gui --features daynaport daynaport = ["iris/daynaport"] -# Emulate an R5000 CPU instead of the default R4400 (compile-time: the cache -# model differs deeply). Surfaced read-only on the Memory tab via -# iris::build_features::CPU. Build with: cargo build -p iris-gui --features r5k +# Vestigial for CPU selection. The R4400/R5000 choice is a runtime setting on +# the Memory tab (`[machine] cpu`), and both models are compiled into every +# build — see the note in src/lib.rs. This passthrough remains only so the +# feature name still resolves for anyone building with it. r5k = ["iris/r5k"] # YouTube / max in-process perf: lightning (no GDB hot-path) + idle-pause on the # embedded iris core. Build with: cargo build -p iris-gui --release --features premiere diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index db42673..e285578 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -2,7 +2,7 @@ use egui::{Color32, ComboBox, DragValue, Grid, RichText, ScrollArea, TextEdit, U use iris::build_features; use std::path::Path; use iris::config::{ - ForwardBind, ForwardProto, GraphicsBoard, MachineConfig, MachineProfile, NetMode, + CpuModel, ForwardBind, ForwardProto, GraphicsBoard, MachineConfig, MachineProfile, NetMode, NfsConfig, PortForwardConfig, ScsiDeviceConfig, ScsiKind, VinoSource, VinoStandard, VALID_BANK_SIZES, }; @@ -16,6 +16,9 @@ use crate::ram::{ram_summary, RAM_PRESETS}; pub struct MemoryUiContext { pub running: bool, pub started_banks: Option<[u32; 4]>, + /// The CPU the running guest actually booted on, so a pending change is + /// shown as pending rather than as fact. `None` when nothing is running. + pub started_cpu: Option, } /// A host network interface candidate for the PCAP backend selector. This is a @@ -360,14 +363,43 @@ fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) { ui.heading("Processor"); Grid::new("cpu_grid").num_columns(2).striped(true).show(ui, |ui| { ui.label("CPU"); - ui.label(RichText::new(build_features::CPU).strong()); + // A real setting, not a read-out of how this binary was built. Both + // cache models are compiled in and `Machine::new` picks between them at + // construction, so the choice belongs to the machine's config the same + // way its RAM does. + ui.add_enabled_ui(!mem_ctx.running, |ui| { + ComboBox::from_id_salt("cpu_model") + .selected_text(cfg.machine.cpu.label()) + .show_ui(ui, |ui| { + for c in CpuModel::ALL { + ui.selectable_value(&mut cfg.machine.cpu, c, c.label()); + } + }); + }); ui.end_row(); }); + + if mem_ctx.running { + ui.label( + RichText::new("Stop the VM to change the CPU — it is chosen when the machine starts.") + .color(Color32::from_rgb(220, 170, 90)), + ); + if let Some(started) = mem_ctx.started_cpu { + if started != cfg.machine.cpu { + ui.label(format!("Running guest: {} · Config (pending): {}", + started.label(), cfg.machine.cpu.label())); + } + } + } else { + ui.label(RichText::new("Applied at next Start").weak()); + } ui.label(RichText::new( - "The CPU is fixed at build time — the R4400 and R5000 differ in their cache \ - architecture, so it can't be switched at runtime. To use a different CPU, \ - download that build.") - .weak()); + "The R4400 has 16 KB direct-mapped caches and is MIPS III. The R5000 has \ + 2-way 32 KB caches, no secondary cache, and adds the MIPS IV instructions — \ + so an R4400 raises Reserved Instruction on the ones an R5000 executes. IRIX \ + reads the CPU from PRId and configures itself accordingly, so switching is a \ + different machine to the guest, not a speed knob.") + .weak().small()); ui.separator(); ui.heading("Memory"); @@ -1334,8 +1366,10 @@ fn show_debug(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { let mut action = ConfigAction::None; ui.heading("Build features"); Grid::new("build_features_grid").num_columns(2).striped(true).show(ui, |ui| { - ui.label("CPU"); - ui.label(build_features::CPU); + // What this machine is configured to be, not how the binary was built — + // both cache models are always compiled in and selected at start. + ui.label("CPU (configured)"); + ui.label(cfg.machine.cpu.label()); ui.end_row(); ui.label("REX3 JIT"); ui.label(if build_features::REX_JIT { "enabled at compile time" } else { "not built" }); diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index 055b66d..a398814 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -159,6 +159,8 @@ struct App { cfg_dirty_since: Option, /// Banks passed to the last `Cmd::Start` (guest-visible RAM is fixed until Stop). started_banks: Option<[u32; 4]>, + /// The CPU the running guest booted on — see `MemoryUiContext::started_cpu`. + started_cpu: Option, tab: Tab, /// Benchmark tab state: the child process, its streamed output, and the /// last outcome. Lives here rather than in the tab function because a run @@ -465,6 +467,7 @@ impl App { cfg_dirty: false, cfg_dirty_since: None, started_banks: None, + started_cpu: None, tab: Tab::General, bench: bench_ui::BenchState::default(), emu: EmulatorHandle::spawn(), @@ -748,6 +751,7 @@ impl App { (iris::config::NetMode::Nat, None) }); self.started_banks = Some(self.cfg.banks); + self.started_cpu = Some(self.cfg.machine.cpu); self.emu.send(Cmd::Start(Box::new(self.cfg.clone()))); // Don't resize the window when the VM launches — its size is latched at // app load (the saved window size, or the first-launch fit to vm_scale) @@ -2215,6 +2219,7 @@ impl App { MemoryUiContext { running: self.emu.is_running(), started_banks: self.started_banks, + started_cpu: self.started_cpu, }, &mut self.bench, ); diff --git a/src/config.rs b/src/config.rs index d9ca79b..1d437f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -512,8 +512,9 @@ impl Default for GraphicsSection { /// Emulated CPU. Runtime-selectable: each model is its own monomorphisation, /// so the hot path carries no per-model branch. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, clap::ValueEnum)] #[serde(rename_all = "snake_case")] +#[clap(rename_all = "lower")] pub enum CpuModel { /// MIPS R4400, 16K direct-mapped L1s, 1 MB L2. The Indy IRIS has always shipped. #[default] @@ -1331,6 +1332,15 @@ pub struct Cli { #[arg(long = "no-scsi-deferred-int", default_value_t = false)] pub no_scsi_deferred_int: bool, + /// Emulated CPU: `r4400` (default) or `r5000`. Overrides `[machine] cpu`. + /// + /// Both models are compiled into every build — each is its own + /// monomorphisation, so the hot path carries no per-model branch — and this + /// picks between them at construction. The `r5k` cargo feature no longer + /// selects the model and is vestigial for that purpose. + #[arg(long = "cpu", value_name = "MODEL")] + pub cpu: Option, + /// Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). /// Connect with: target remote localhost: #[arg(long = "gdb-port", value_name = "PORT")] @@ -1425,6 +1435,7 @@ impl Cli { if let Some(p) = self.cdrom6.clone() { apply_scsi(&mut cfg.scsi, 6, p, true, self.cdrom6_extra.clone()); } if let Some(p) = self.scsi7.clone() { apply_scsi(&mut cfg.scsi, 7, p, false, vec![]); } + if let Some(cpu) = self.cpu { cfg.machine.cpu = cpu; } if self.scale2x { cfg.scale = 2; } if self.headless { cfg.headless = true; } if self.no_audio { cfg.no_audio = true; } diff --git a/src/lib.rs b/src/lib.rs index bc380ea..36f60c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,19 +56,14 @@ pub mod build_features { /// monitor breakpoints) is non-functional in this build. pub const LIGHTNING: bool = cfg!(feature = "lightning"); pub const IDLE_PAUSE: bool = cfg!(feature = "idle-pause"); - /// The emulated CPU, fixed at build time (the cache model differs deeply - /// between the R4400 and R5000, so it's a compile-time choice, not a runtime - /// setting). `r5k` selects the R5000; `r5ksc`/`r5ksc_triton` add a secondary - /// cache. The GUI surfaces this read-only on the Memory tab. - pub const CPU: &str = if cfg!(feature = "r5ksc_triton") { - "MIPS R5000 (Triton on-die L2)" - } else if cfg!(all(feature = "r5k", feature = "r5ksc")) { - "MIPS R5000 (external L2)" - } else if cfg!(feature = "r5k") { - "MIPS R5000" - } else { - "MIPS R4400" - }; + // There is deliberately no `CPU` constant here any more. The emulated CPU + // stopped being a build-time property in 96e5ddd: both cache models are + // monomorphised into every binary and `Machine::new` picks between them + // from `cfg.machine.cpu`. A constant derived from cargo features could only + // report how the binary was compiled, which is no longer the same question + // as which CPU is running — and it was being displayed to users as though + // it were. Ask the config (`MachineConfig::machine.cpu`), or the guest, + // which reads PRId. /// Every compile-time flag this binary was built with, in a fixed order. /// From af446663a3e7cd40971c9021f78ed67787a6379f Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 23:32:58 -0400 Subject: [PATCH 5/9] fix six CpuDevice methods that recursed instead of forwarding CpuDevice's developer-only forwarders called MipsCpu::method(self) meaning the inherent method, but those six inherent methods are behind #[cfg(feature = "developer")]. In a build without it the inherent method does not exist, so the path resolved to the trait method being defined and the call recursed until the stack ran out. The six machine.rs wrappers that reach them are already developer-gated, so the trait declarations and impls now carry the same gates (set_jitv2_dispatch_enabled needs jitv2 + developer, matching its inherent counterpart). rustc's unconditional_recursion lint caught this; it was reachable, not theoretical. Also cleaned up while here: - four unnecessary braces left over from turning #[cfg(feature = "r5k")] blocks into `if Self::IS_R5K`; the L1 index now reads as one binding with the way OR'd in for the 2-way model instead of two braced arms. - fetch_update -> try_update in rex3 and jitv2. Renamed upstream for consistency, stable since 1.95, same semantics and arguments. - cfg_aliases 0.2.1 -> 0.2.2, which is where the vendored winit build script's 91 "trailing semicolon in macro" warnings came from. The requirement is a caret, so a fresh resolve picks it up without a Cargo.lock change (that file is gitignored). Both crates are now warning-free: iris across twelve feature combinations and iris-gui across seven, all with --all-targets. Tests 425 default, 428 developer, 424 r5k. Co-Authored-By: Claude Opus 5 (1M context) --- src/jitv2/jitv2.rs | 4 ++-- src/mips_cache_v2.rs | 15 +++++---------- src/mips_exec.rs | 12 ++++++++++++ src/rex3.rs | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/jitv2/jitv2.rs b/src/jitv2/jitv2.rs index ad67350..2fe5693 100644 --- a/src/jitv2/jitv2.rs +++ b/src/jitv2/jitv2.rs @@ -942,8 +942,8 @@ impl PhysicalCodePage { // Relaxed CAS loop: this is a diagnostic counter with no ordering // relationship to any other state, and contention is effectively nil // (compiles for a given page are serialized), so a plain - // fetch_update-style saturating add is all that's needed. - let _ = c.fetch_update( + // try_update-style saturating add is all that's needed. + let _ = c.try_update( std::sync::atomic::Ordering::Relaxed, std::sync::atomic::Ordering::Relaxed, |v| Some(v.saturating_add(1)), diff --git a/src/mips_cache_v2.rs b/src/mips_cache_v2.rs index 1deb91e..7319e0c 100644 --- a/src/mips_cache_v2.rs +++ b/src/mips_cache_v2.rs @@ -2323,20 +2323,15 @@ impl> 14) as usize & 1) << Self::IC_NUM_LINES_SHIFT) } - } else { - { self.ic.get_index(virt_addr) } - } + let set = self.ic.get_index(virt_addr); + if Self::IS_R5K { set | (((virt_addr >> 14) as usize & 1) << Self::IC_NUM_LINES_SHIFT) } else { set } } else { - if Self::IS_R5K { - { self.dc.get_index(virt_addr) | (((virt_addr >> 14) as usize & 1) << Self::DC_NUM_LINES_SHIFT) } - } else { - { self.dc.get_index(virt_addr) } - } + let set = self.dc.get_index(virt_addr); + if Self::IS_R5K { set | (((virt_addr >> 14) as usize & 1) << Self::DC_NUM_LINES_SHIFT) } else { set } }; #[cfg(feature = "debug_cache")] diff --git a/src/mips_exec.rs b/src/mips_exec.rs index 14168a3..5fda05e 100644 --- a/src/mips_exec.rs +++ b/src/mips_exec.rs @@ -11710,12 +11710,18 @@ pub trait CpuDevice: Device + Resettable + Saveable + Send + Sync { fn load_elf(&self, path: &str) -> Result; fn load_elf_bytes(&self, bytes: &[u8], name: &str) -> Result; fn step_n_inline(&self, n: u64) -> Result; + #[cfg(feature = "developer")] fn step_one_inline_counting_instructions(&self) -> Result; fn state_digest(&self) -> Result; + #[cfg(feature = "developer")] fn restore_state_digest(&self, d: &CpuStateDigest) -> Result<(), String>; + #[cfg(feature = "developer")] fn fixup_cp0_count(&self, d: &CpuStateDigest) -> Result<(), String>; + #[cfg(all(feature = "jitv2", feature = "developer"))] fn set_jitv2_dispatch_enabled(&self, on: bool) -> Result; + #[cfg(feature = "developer")] fn set_hw_read_fixup_recording(&self, on: bool) -> Result<(), String>; + #[cfg(feature = "developer")] fn set_hw_read_fixup_replay(&self, f: Option>) -> Result<(), String>; /// CPU model name, as the guest and the benchmark report see it. fn model_name(&self) -> &'static str; @@ -11739,14 +11745,20 @@ impl CpuDevice for MipsCp fn load_elf(&self, p: &str) -> Result { MipsCpu::load_elf(self, p) } fn load_elf_bytes(&self, b: &[u8], n: &str) -> Result { MipsCpu::load_elf_bytes(self, b, n) } fn step_n_inline(&self, n: u64) -> Result { MipsCpu::step_n_inline(self, n) } + #[cfg(feature = "developer")] fn step_one_inline_counting_instructions(&self) -> Result { MipsCpu::step_one_inline_counting_instructions(self) } fn state_digest(&self) -> Result { MipsCpu::state_digest(self) } + #[cfg(feature = "developer")] fn restore_state_digest(&self, d: &CpuStateDigest) -> Result<(), String> { MipsCpu::restore_state_digest(self, d) } + #[cfg(feature = "developer")] fn fixup_cp0_count(&self, d: &CpuStateDigest) -> Result<(), String> { MipsCpu::fixup_cp0_count(self, d) } + #[cfg(all(feature = "jitv2", feature = "developer"))] fn set_jitv2_dispatch_enabled(&self, on: bool) -> Result { MipsCpu::set_jitv2_dispatch_enabled(self, on) } + #[cfg(feature = "developer")] fn set_hw_read_fixup_recording(&self, on: bool) -> Result<(), String> { MipsCpu::set_hw_read_fixup_recording(self, on) } + #[cfg(feature = "developer")] fn set_hw_read_fixup_replay(&self, f: Option>) -> Result<(), String> { MipsCpu::set_hw_read_fixup_replay(self, f) } diff --git a/src/rex3.rs b/src/rex3.rs index cabc321..41df095 100644 --- a/src/rex3.rs +++ b/src/rex3.rs @@ -3350,7 +3350,7 @@ impl Rex3 { #[cfg(feature = "developer")] { let len = self.gfifo.len() + 1; - let _ = self.gfifo_hwm.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |hwm| { + let _ = self.gfifo_hwm.try_update(Ordering::Relaxed, Ordering::Relaxed, |hwm| { if len > hwm { Some(len) } else { None } }); } From cf9b01d2bfcfa4935709cc516f84583fcad655f7 Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Fri, 21 Aug 2026 23:48:01 -0400 Subject: [PATCH 6/9] bench: measure the CPU you picked, not always an R4400 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 52c0a23 made the CPU a runtime setting and fixed the CI matrices and cpu-tests/run/matrix.sh, but the benchmark suite still had three ways of getting an R4400 no matter what was asked for. `bench_config` built its MachineConfig with `..Default::default()`, so every in-process run — the GUI's Benchmark tab and `iris-bench run` without --iris — measured an R4400. BenchOptions now carries the model. `iris-bench matrix`'s cells still selected the CPU with `features: "r5k"`, which stopped selecting anything at 96e5ddd. Its r5000 cells were building a second emulator to get a machine identical to the r4400 cell's; the expect_cpu guard would have caught it on the next run. A cell is now a feature set plus a --cpu, and the cached binary is keyed on the feature set, so cells differing only by CPU share one build: r4400-interp (default) r4400 ┐ one binary r5000-interp (default) r5000 ┘ r4400-jitv2 jitv2 r4400 ┐ one binary r5000-jitv2 jitv2 r5000 ┘ Six cells, four builds instead of six. The GUI's Benchmark tab had no CPU control at all; it gets a combo box next to Quick. `iris-bench run` gets --cpu for the in-process path (with --iris, forward it to the emulator: `-- --cpu r5000`). Verified rather than assumed. One binary, in-process: --cpu r4400 -> 51.6 MIPS 100.0% accuracy --cpu r5000 -> 43.7 MIPS 100.0% accuracy and the full matrix now reports the CPU each cell asked for — R4400, R4400, R5000, R5000 — where before every cell said R4400. All four at 40/40, and every delta against the pre-change baseline (+4.2, -3.6, +0.4, -0.2 %) is inside this host's ~4.5% run-to-run noise, measured earlier by re-running identical binaries. Note bench/build/ still holds stale per-cell binaries from the old naming (iris-r4400-interp and friends). Nothing reads them now; they can be deleted. Co-Authored-By: Claude Opus 5 (1M context) --- iris-gui/src/bench_ui.rs | 29 ++++++++++++++++++++++--- src/bench_runner.rs | 13 +++++++---- src/bin/iris_bench.rs | 47 +++++++++++++++++++++++++--------------- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/iris-gui/src/bench_ui.rs b/iris-gui/src/bench_ui.rs index 47dc4a6..ed80edc 100644 --- a/iris-gui/src/bench_ui.rs +++ b/iris-gui/src/bench_ui.rs @@ -42,6 +42,8 @@ pub struct BenchState { /// worker is gone. last: Option, String>>, quick: bool, + /// Which CPU to benchmark. Runtime config, so this needs no separate build. + cpu: iris::config::CpuModel, /// Parsed once. Normally empty — see `iris::bench_report::bundled_reference`. reference: Option, dev: DevRunner, @@ -59,6 +61,7 @@ impl Default for BenchState { // number, and is required before a result can go in the reference // table. quick: true, + cpu: iris::config::CpuModel::default(), reference: None, dev: DevRunner::default(), } @@ -98,7 +101,7 @@ impl BenchState { self.live.is_some() || self.dev.is_running() } - fn start(&mut self, quick: bool) { + fn start(&mut self, quick: bool, cpu: iris::config::CpuModel) { if self.is_running() { return; } let lines = Arc::new(Mutex::new(Vec::new())); @@ -108,6 +111,7 @@ impl BenchState { let opts = BenchOptions { quick, + cpu, label: iris::bench_report::cpu_model(), cancel: Some(cancel.clone()), ..Default::default() @@ -228,11 +232,30 @@ fn controls(ui: &mut Ui, st: &mut BenchState, machine_running: bool) { .on_hover_text(format!("About {secs} seconds. Runs entirely inside this app.")) .clicked() { - let quick = st.quick; - st.start(quick); + let (quick, cpu) = (st.quick, st.cpu); + st.start(quick, cpu); } }); + ui.add_enabled_ui(!busy, |ui| { + // The CPU is a machine setting, not a build, so the suite can measure + // either one from this binary. IRIX-visible: different PRId, cache + // geometry and ISA level, so treat the two scores as different machines. + egui::ComboBox::from_id_salt("bench_cpu") + .selected_text(st.cpu.label()) + .show_ui(ui, |ui| { + for m in iris::config::CpuModel::ALL { + ui.selectable_value(&mut st.cpu, m, m.label()); + } + }) + .response + .on_hover_text( + "Which CPU to benchmark. R4400 is 16 KB direct-mapped L1s with a \ + secondary cache and MIPS III; R5000 is 2-way 32 KB L1s, no \ + secondary cache and MIPS IV. Scores are not comparable between them.", + ); + }); + ui.add_enabled_ui(!busy, |ui| { ui.checkbox(&mut st.quick, "Quick") .on_hover_text( diff --git a/src/bench_runner.rs b/src/bench_runner.rs index d57c84c..a7869fd 100644 --- a/src/bench_runner.rs +++ b/src/bench_runner.rs @@ -22,7 +22,7 @@ use parking_lot::Mutex; use crate::bench_report::{host_info, parse_block, Run}; use crate::benchsuite; -use crate::config::MachineConfig; +use crate::config::{CpuModel, MachineConfig}; use crate::machine::Machine; use crate::testdev::{RunConfig, TestDevice}; @@ -57,6 +57,9 @@ pub struct BenchOptions { pub banks: [u32; 4], /// A hang detector, not a performance budget. pub timeout: Duration, + /// Emulated CPU. Runtime config since 96e5ddd, so the in-process runner picks + /// it like any other machine setting rather than needing its own build. + pub cpu: CpuModel, /// Set this to abort. The machine is stopped and `run` returns an error — /// there is no partial report, because the suite prints its block only at /// the end. Exists so an interactive caller's Stop button does something @@ -70,6 +73,7 @@ impl Default for BenchOptions { quick: false, label: "local".to_string(), banks: BENCH_BANKS, + cpu: CpuModel::default(), timeout: Duration::from_secs(1800), cancel: None, } @@ -139,7 +143,7 @@ fn run_inner( let started = Instant::now(); let mut machine = Box::new(Machine::new_with_testdev( - bench_config(opts.banks), + bench_config(opts.banks, opts.cpu), Some(testdev), )); // Deliberately no `register_system_controller`: it hands a raw pointer to @@ -290,7 +294,7 @@ impl Table { /// The bare-metal machine the suite runs on: RAM, a test device, and nothing /// else. No SCSI (there is no disk image and no filesystem to find one on), no /// graphics, no audio. -pub fn bench_config(banks: [u32; 4]) -> MachineConfig { +pub fn bench_config(banks: [u32; 4], cpu: CpuModel) -> MachineConfig { let mut cfg = MachineConfig { banks, headless: true, @@ -301,6 +305,7 @@ pub fn bench_config(banks: [u32; 4]) -> MachineConfig { // when the file is absent — and here it always is. Same reason // bench/run/bare.toml carries a present-but-empty `[scsi]`. cfg.scsi.clear(); + cfg.machine.cpu = cpu; cfg } @@ -382,7 +387,7 @@ mod tests { #[test] fn the_bench_machine_has_no_disks() { - let cfg = bench_config(BENCH_BANKS); + let cfg = bench_config(BENCH_BANKS, CpuModel::default()); assert!(cfg.scsi.is_empty(), "a bench machine must not try to open a disk image"); assert!(cfg.headless && cfg.no_audio); } diff --git a/src/bin/iris_bench.rs b/src/bin/iris_bench.rs index 2e47698..4a6947b 100644 --- a/src/bin/iris_bench.rs +++ b/src/bin/iris_bench.rs @@ -147,11 +147,12 @@ fn run_host(exe: &Path, timeout_s: u64) -> Result { /// for a minute is not mistaken for a hang, and that property is worth keeping /// at the command line — so this echoes every line rather than waiting for the /// end and printing the parsed summary. -fn run_embedded(label: &str, timeout_s: u64, quick: bool) -> Result { +fn run_embedded(label: &str, timeout_s: u64, quick: bool, cpu: iris::config::CpuModel) -> Result { let opts = BenchOptions { quick, label: label.to_string(), timeout: Duration::from_secs(timeout_s), + cpu, ..Default::default() }; bench_runner::run(&opts, |p| { @@ -243,12 +244,16 @@ fn load_all(dir: &Path) -> Result, String> { // ─── the matrix ────────────────────────────────────────────────────────────── -/// A cell is a cargo feature set plus the CPU the guest must report. The CPU -/// model and the JIT are compile-time features, so each cell is a separate -/// build of the emulator — there is no runtime switch to flip. +/// A cell is a cargo feature set plus a CPU. Only the feature set needs a build: +/// the CPU is a runtime setting, so the r4400 and r5000 cells of an engine share +/// one binary. `expect_cpu` is still checked, because a cell silently running +/// the wrong CPU is a mistake this repo has made before. struct Cell { name: &'static str, features: &'static str, + /// Passed as `--cpu`. Runtime since 96e5ddd, so cells that differ only by + /// CPU share one emulator binary instead of each needing its own build. + cpu: &'static str, /// What the guest must print in `#machine cpu=`. Checked, because an /// overwritten target/release/iris silently turning an "R4400" cell into /// an R5000 run is a mistake this repo has made before — see @@ -257,16 +262,19 @@ struct Cell { } const CELLS: &[Cell] = &[ - Cell { name: "r4400-interp", features: "", expect_cpu: "R4400" }, - Cell { name: "r5000-interp", features: "r5k", expect_cpu: "R5000" }, - Cell { name: "r4400-jitv2", features: "jitv2", expect_cpu: "R4400" }, - Cell { name: "r5000-jitv2", features: "r5k,jitv2", expect_cpu: "R5000" }, - Cell { name: "r4400-lightning", features: "lightning", expect_cpu: "R4400" }, - Cell { name: "r4400-jitv2-lightning", features: "jitv2,lightning", expect_cpu: "R4400" }, + Cell { name: "r4400-interp", features: "", cpu: "r4400", expect_cpu: "R4400" }, + Cell { name: "r5000-interp", features: "", cpu: "r5000", expect_cpu: "R5000" }, + Cell { name: "r4400-jitv2", features: "jitv2", cpu: "r4400", expect_cpu: "R4400" }, + Cell { name: "r5000-jitv2", features: "jitv2", cpu: "r5000", expect_cpu: "R5000" }, + Cell { name: "r4400-lightning", features: "lightning", cpu: "r4400", expect_cpu: "R4400" }, + Cell { name: "r4400-jitv2-lightning", features: "jitv2,lightning", cpu: "r4400", expect_cpu: "R4400" }, ]; fn build_cell(cell: &Cell, root: &Path, force: bool) -> Result { - let dest = root.join("bench/build").join(format!("iris-{}", cell.name)); + // Keyed by feature set: cells differing only in --cpu share a binary. + let slug = if cell.features.is_empty() { "default".to_string() } + else { cell.features.replace(',', "-") }; + let dest = root.join("bench/build").join(format!("iris-{}", slug)); if dest.exists() && !force { println!(" reusing {}", dest.display()); return Ok(dest); @@ -711,6 +719,10 @@ enum Cmd { /// the register that carries it is set at machine construction. #[arg(long)] quick: bool, + /// Emulated CPU for the in-process run. With --iris, forward it to the + /// emulator instead: `-- --cpu r5000`. + #[arg(long, default_value = "r4400")] + cpu: iris::config::CpuModel, /// Extra arguments passed through to --iris. #[arg(last = true)] extra: Vec, @@ -824,15 +836,15 @@ fn main() { fn dispatch(cmd: Cmd) -> Result<(), String> { match cmd { Cmd::Cells => { - println!("{:<24} {}", "cell", "cargo features"); + println!("{:<24} {:<22} {}", "cell", "cargo features", "--cpu"); for c in CELLS { - println!("{:<24} {}", c.name, - if c.features.is_empty() { "(default)" } else { c.features }); + println!("{:<24} {:<22} {}", c.name, + if c.features.is_empty() { "(default)" } else { c.features }, c.cpu); } Ok(()) } - Cmd::Run { iris, elf, config, label, out, timeout, quick, extra } => { + Cmd::Run { iris, elf, config, label, out, timeout, quick, cpu, extra } => { let out = out.unwrap_or_else(default_out); let run = match iris { Some(iris) => { @@ -861,7 +873,7 @@ fn dispatch(cmd: Cmd) -> Result<(), String> { "{} only {} to --iris, and `run` is in-process by default. \ Add --iris PATH, or drop {}.", stray.join(" and "), verb, obj)); } - run_embedded(&label, timeout, quick)? + run_embedded(&label, timeout, quick, cpu)? } }; let path = save(&run, &out)?; @@ -915,7 +927,8 @@ fn dispatch(cmd: Cmd) -> Result<(), String> { Ok(p) => p, Err(e) => { eprintln!(" {}", e); failures.push(cell.name); continue; } }; - match run_guest(&iris, &elf, &config, cell.name, timeout, &[]) { + let cpu_arg = ["--cpu".to_string(), cell.cpu.to_string()]; + match run_guest(&iris, &elf, &config, cell.name, timeout, &cpu_arg) { Ok(run) => { // The guest reads PRId, so its banner is the authority // on which CPU actually ran. From 9300fd5702f8cce2870e0229f2b99943719cf8fd Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Sat, 22 Aug 2026 05:53:34 -0400 Subject: [PATCH 7/9] iris-gui: surface the CPU everywhere the machine is described MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CPU became a per-machine setting in 9ef7b06, but the only place to see or change it was a combo box on the Memory tab — which is the wrong tab for it, and left three places describing a machine without mentioning which processor it has. - New Machine now offers the CPU next to the machine model, so a machine starts life with the one you wanted instead of being created as an R4400 and edited afterwards. - The left-hand machine summary gains a Processor row, and a "Processor (last Start)" row when the running guest differs from the config — the same shape the RAM rows already use. - Processor moves from Memory to General. It sits with Platform, which is the other thing that decides what machine the guest thinks it is; RAM is a quantity, this is an identity. - The Machine menu gets the selection too, mirroring the Memory menu: radio buttons disabled while running, "Running: X (Stop to apply edits)" in yellow when the config has moved on, and "CPU changes apply after Stop → Start" underneath. Picking one toasts "applies at next Start". R4400 remains the default in every one of them: CpuModel::default(). No CLI work was needed — `iris --cpu r4400|r5000` already exists from 85e44a4 and defaults to r4400. iris-ci drives an already-running emulator over the control socket and never constructs a machine, so it needs no switch of its own: launch the emulator with `iris --ci --cpu r5000` and iris-ci attaches to whatever is running. Checked by launching the GUI, not only by compiling it: it stays up with no panic and no egui id-collision warnings, and the two panels these changes touch are the ones that render on startup (General is the default tab, welcome_panel shows while stopped). The menu and the modal need a click, so those are compile- and id-checked only — every from_id_salt in the crate is unique. No screenshot: this box has no Xvfb or capture tools. Clean across default, appstore, bundled, premiere and pcap. Co-Authored-By: Claude Opus 5 (1M context) --- iris-gui/src/config_ui.rs | 91 +++++++++++++++-------------- iris-gui/src/dialogs/new_machine.rs | 15 ++++- iris-gui/src/main.rs | 40 +++++++++++++ 3 files changed, 100 insertions(+), 46 deletions(-) diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index e285578..c4070a9 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -192,7 +192,7 @@ pub fn show_tab( bench: &mut crate::bench_ui::BenchState, ) -> TabOutcome { ScrollArea::vertical().show(ui, |ui| match tab { - Tab::General => TabOutcome { action: show_general(ui, cfg), ..Default::default() }, + Tab::General => TabOutcome { action: show_general(ui, cfg, mem_ctx), ..Default::default() }, Tab::Disks => { let (e, a) = show_disks(ui, cfg); TabOutcome { action: a, disks_changed: e.changed, disk_picked: e.picked, ..Default::default() } } Tab::Network => { let net = show_network(ui, cfg, host, disk_folders, pcap_ifaces); @@ -207,7 +207,7 @@ pub fn show_tab( }).inner } -fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { +fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) -> ConfigAction { let mut action = ConfigAction::None; ui.heading("General"); ui.label(format!("Platform: {}", cfg.machine.profile.label())); @@ -245,6 +245,48 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { .small(), ); } + ui.heading("Processor"); + Grid::new("cpu_grid").num_columns(2).striped(true).show(ui, |ui| { + ui.label("CPU"); + // A real setting, not a read-out of how this binary was built. Both + // cache models are compiled in and `Machine::new` picks between them at + // construction, so the choice belongs to the machine's config the same + // way its RAM does. + ui.add_enabled_ui(!mem_ctx.running, |ui| { + ComboBox::from_id_salt("cpu_model") + .selected_text(cfg.machine.cpu.label()) + .show_ui(ui, |ui| { + for c in CpuModel::ALL { + ui.selectable_value(&mut cfg.machine.cpu, c, c.label()); + } + }); + }); + ui.end_row(); + }); + + if mem_ctx.running { + ui.label( + RichText::new("Stop the VM to change the CPU — it is chosen when the machine starts.") + .color(Color32::from_rgb(220, 170, 90)), + ); + if let Some(started) = mem_ctx.started_cpu { + if started != cfg.machine.cpu { + ui.label(format!("Running guest: {} · Config (pending): {}", + started.label(), cfg.machine.cpu.label())); + } + } + } else { + ui.label(RichText::new("Applied at next Start").weak()); + } + ui.label(RichText::new( + "The R4400 has 16 KB direct-mapped caches and is MIPS III. The R5000 has \ + 2-way 32 KB caches, no secondary cache, and adds the MIPS IV instructions — \ + so an R4400 raises Reserved Instruction on the ones an R5000 executes. IRIX \ + reads the CPU from PRId and configures itself accordingly, so switching is a \ + different machine to the guest, not a speed knob.") + .weak().small()); + ui.separator(); + ui.horizontal(|ui| { ui.label("Newport heads"); ui.add(egui::DragValue::new(&mut cfg.graphics.heads).range(1..=2).speed(0.1)); @@ -296,7 +338,8 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { // external gopher64 process it talks to. #[cfg(not(feature = "appstore"))] { - ui.separator(); + + ui.separator(); #[cfg(feature = "ultra64")] ui.checkbox(&mut cfg.ultra64.enabled, "N64 development board (Ultra64)") .on_hover_text( @@ -360,48 +403,6 @@ fn show_resolution_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { } fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) { - ui.heading("Processor"); - Grid::new("cpu_grid").num_columns(2).striped(true).show(ui, |ui| { - ui.label("CPU"); - // A real setting, not a read-out of how this binary was built. Both - // cache models are compiled in and `Machine::new` picks between them at - // construction, so the choice belongs to the machine's config the same - // way its RAM does. - ui.add_enabled_ui(!mem_ctx.running, |ui| { - ComboBox::from_id_salt("cpu_model") - .selected_text(cfg.machine.cpu.label()) - .show_ui(ui, |ui| { - for c in CpuModel::ALL { - ui.selectable_value(&mut cfg.machine.cpu, c, c.label()); - } - }); - }); - ui.end_row(); - }); - - if mem_ctx.running { - ui.label( - RichText::new("Stop the VM to change the CPU — it is chosen when the machine starts.") - .color(Color32::from_rgb(220, 170, 90)), - ); - if let Some(started) = mem_ctx.started_cpu { - if started != cfg.machine.cpu { - ui.label(format!("Running guest: {} · Config (pending): {}", - started.label(), cfg.machine.cpu.label())); - } - } - } else { - ui.label(RichText::new("Applied at next Start").weak()); - } - ui.label(RichText::new( - "The R4400 has 16 KB direct-mapped caches and is MIPS III. The R5000 has \ - 2-way 32 KB caches, no secondary cache, and adds the MIPS IV instructions — \ - so an R4400 raises Reserved Instruction on the ones an R5000 executes. IRIX \ - reads the CPU from PRId and configures itself accordingly, so switching is a \ - different machine to the guest, not a speed knob.") - .weak().small()); - ui.separator(); - ui.heading("Memory"); if mem_ctx.running { ui.label( diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index 3eec774..c78de47 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -1,5 +1,5 @@ use eframe::egui::{self, Color32, ComboBox, Grid, RichText, TextEdit}; -use iris::config::{MachineConfig, MachineProfile, ScsiDeviceConfig, VALID_BANK_SIZES}; +use iris::config::{CpuModel, MachineConfig, MachineProfile, ScsiDeviceConfig, VALID_BANK_SIZES}; use iris::vc2_timings::NewportResolution; use crate::ram::RAM_PRESETS; @@ -11,6 +11,7 @@ pub struct NewMachineDialog { pub name: String, /// Emulated SGI machine model (Indy IP24 / Indigo2 IP22). pub profile: MachineProfile, + pub cpu: CpuModel, /// Host-forced Newport video mode (`Guest` leaves it to IRIX/setmon). pub resolution: NewportResolution, pub prom_path: String, @@ -38,6 +39,7 @@ impl Default for NewMachineDialog { open: false, name: "indy".into(), profile: MachineProfile::default(), + cpu: CpuModel::default(), resolution: NewportResolution::default(), prom_path: "prom.bin".into(), use_embedded_prom: true, @@ -99,6 +101,16 @@ impl NewMachineDialog { }); ui.end_row(); + ui.label("Processor"); + ComboBox::from_id_salt("nm_cpu") + .selected_text(self.cpu.label()) + .show_ui(ui, |ui| { + for c in CpuModel::ALL { + ui.selectable_value(&mut self.cpu, c, c.label()); + } + }); + ui.end_row(); + ui.label("Display resolution"); ComboBox::from_id_salt("nm_resolution") .selected_text(self.resolution.label()) @@ -235,6 +247,7 @@ impl NewMachineDialog { { let mut cfg = MachineConfig::default(); cfg.machine.profile = self.profile; + cfg.machine.cpu = self.cpu; cfg.graphics.resolution = self.resolution; cfg.prom = if self.use_embedded_prom { String::new() } else { self.prom_path.clone() }; // Empty prom path makes Machine::new fall back to embedded diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index a398814..ff3fab5 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -1206,6 +1206,34 @@ impl App { ui.close(); } ui.separator(); + // Chosen at Machine::new, so an edit while running is pending, not live — + // same shape as the Memory menu's RAM rows. + ui.set_min_width(240.0); + ui.label(RichText::new(format!("Processor: {}", self.cfg.machine.cpu.label())).strong()); + ui.add_enabled_ui(!running, |ui| { + for c in iris::config::CpuModel::ALL { + if ui.radio(self.cfg.machine.cpu == c, c.label()).clicked() { + self.cfg.machine.cpu = c; + self.mark_dirty(); + self.toast(format!("{} — applies at next Start", c.label())); + } + } + }); + if running { + match self.started_cpu { + Some(started) if started != self.cfg.machine.cpu => { + ui.label(RichText::new(format!( + "Running: {} (Stop to apply edits)", started.label())) + .color(Color32::YELLOW)); + } + Some(started) => { + ui.label(RichText::new(format!("Running: {}", started.label())).weak()); + } + None => {} + } + ui.label(RichText::new("CPU changes apply after Stop → Start").weak().small()); + } + ui.separator(); ui.horizontal(|ui| { ui.label("Save state:"); ui.add(egui::TextEdit::singleline(&mut self.save_state_name).desired_width(120.0)); @@ -2956,6 +2984,18 @@ impl App { ui.label("Platform"); ui.label(self.cfg.machine.profile.label()); ui.end_row(); + // Guest-visible (PRId, cache geometry, ISA level), so it belongs in + // the summary next to the platform rather than buried in a tab. + ui.label("Processor"); + ui.label(self.cfg.machine.cpu.label()); + ui.end_row(); + if let Some(started) = self.started_cpu { + if started != self.cfg.machine.cpu { + ui.label("Processor (last Start)"); + ui.label(started.label()); + ui.end_row(); + } + } ui.label("PROM"); ui.label(if std::path::Path::new(&self.cfg.prom).exists() { abs_path(&self.cfg.prom) From c98aaaea3cb4a3a5314009b510d8385c396f0b13 Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Sat, 22 Aug 2026 06:26:03 -0400 Subject: [PATCH 8/9] docs, installer: the R5000 is a setting, not a separate download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both described the R5000 as something you build or install instead of the R4400, which stopped being true at 9ef7b06. installer/iris-gui.iss carried a whole second product — "IRIS (R5000)" with its own AppId, name and default folder — built by passing /DR5000=1. Nothing passes it any more, and it would now ship a byte-identical exe under a different name, so the variant is gone and there is one "IRIS". README's section was titled after the cargo feature and told you to run `cargo run --release --features r5k`. It is now "Emulated CPU": a table of what actually differs between the two, and the three ways to pick one (GUI, `[machine] cpu`, `--cpu`). Its performance note was wrong in a way worth not repeating. It claimed "roughly 5% lower instruction throughput" for the R5000 as a flat figure. The sign depends on the engine — measured on this tree, bare-metal suite: interp R4400 52.45 R5000 43.35 R5000 -17.4% jitv2 R4400 192.30 R5000 210.78 R5000 +9.6% Slower under the interpreter, where probing both ways is on the hot path; faster under jitv2, where it isn't and the larger cache with 32-byte lines pays off. The text now says both, and says not to compare scores across CPUs. One consequence to be aware of when releasing: anyone who installed the old "IRIS (R5000)" has it under a different AppId, so the single installer will not upgrade or replace it — it will sit alongside as a separate entry until they uninstall it by hand. Silently orphaning it is the same failure the AppId comment in this file already warns about; there is no Inno mechanism to adopt a foreign AppId, so it needs a release note rather than a code change. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 40 +++++++++++++++++++++++++++++++--------- installer/iris-gui.iss | 21 +++++++++------------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 72c2f94..31584f2 100644 --- a/README.md +++ b/README.md @@ -217,22 +217,44 @@ in the monitor shows its MAC, addresses and counters. Full protocol and verification notes: [docs/daynaport.md](docs/daynaport.md). -## R5000 CPU (`--features r5k`) +## Emulated CPU -Switches the emulated CPU from R4400 to R5000: +R4400 (the default) or R5000, chosen per machine at runtime — not at build time. +Both cache models are compiled into every binary and the machine picks between +them when it starts, so there is no separate R5000 build or download. -- 32KB 2-way set-associative L1I and L1D (32B lines) instead of 16KB direct-mapped (16B lines) -- PRID `0x00002321` (R5000 rev 2.1), FPU FIR `0x00002300` (imp 0x23) -- CP0 Config reports `SC=1` (no secondary cache); PROM uses the 2-way index-flush path +| | R4400 | R5000 | +|---|---|---| +| L1 I/D | 16KB direct-mapped, 16B lines | 32KB 2-way, 32B lines | +| Secondary cache | 1MB unified L2 | none (Config `SC=1`) | +| ISA | MIPS III | MIPS IV | +| PRId / FPU FIR | `0x00000440` / `0x00000500` | `0x00002321` / `0x00002300` | -The 2-way associativity requires probing both ways on every fetch/read/write, which -carries a small performance cost compared to the R4K direct-mapped path — expect -roughly 5% lower instruction throughput. +This is a different machine to the guest, not a speed knob: IRIX reads PRId and +configures itself from it, and an R4400 raises Reserved Instruction on the MIPS IV +opcodes an R5000 executes. +Which one is faster depends on the engine, so don't compare scores across CPUs. +On the bare-metal suite the R5000 is about 17% slower under the interpreter — +2-way associativity means probing both ways on every fetch, read and write — but +about 10% faster under jitv2, where the larger cache and 32-byte lines pay off +and the probe is not on the critical path. + +Pick it in the GUI (Machine menu, or the General tab of the configuration area), +in a config file, or on the command line: + +``` +cargo run --release -- --cpu r5000 # or r4400, the default ``` -cargo run --release --features r5k + +```toml +[machine] +cpu = "r5000" ``` +A snapshot records the CPU it was taken on and refuses to restore onto the other +one, since the captured state assumes that machine. + ## JIT compilers diff --git a/installer/iris-gui.iss b/installer/iris-gui.iss index 51859b5..570d76c 100644 --- a/installer/iris-gui.iss +++ b/installer/iris-gui.iss @@ -38,18 +38,15 @@ #define ArchIdentifier "x64compatible" #endif -; CPU variant of the bundled iris-gui.exe. CI passes /DR5000=1 for the R5000 -; build; without it this is the default R4400 build. The two get distinct AppIds, -; names, and default folders so both can be installed side by side — same binary -; layout, different emulated CPU (a compile-time choice, so they can't be one -; installer with a switch). -#ifdef R5000 - #define MyAppName "IRIS (R5000)" - #define MyAppId "{{2B6E5D74-9C41-4A83-B7F0-5E1C8D2A64B9}" -#else - #define MyAppName "IRIS" - #define MyAppId "{{A7F2C91E-3D8B-4F5A-8E2C-1B9D6A3E8F42}" -#endif +; One installer, both CPUs. There used to be a separate "IRIS (R5000)" product +; with its own AppId, because the emulated CPU was a compile-time cargo feature +; and the two builds genuinely were different programs. It stopped being one: +; both cache models are compiled into every binary and the machine picks between +; them from its config, so a second installer would ship an identical exe under +; a different name. Choose the CPU in the app (Machine menu, or the General tab) +; or on the command line with `--cpu r4400|r5000`. +#define MyAppName "IRIS" +#define MyAppId "{{A7F2C91E-3D8B-4F5A-8E2C-1B9D6A3E8F42}" #define MyAppPublisher "Dani Sarfati" #define MyAppURL "https://github.com/danifunker/iris" #define MyAppExeName "iris-gui.exe" From d6733eedd6018da5bf354972c02096ccab8e7e31 Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Sat, 22 Aug 2026 10:35:04 -0400 Subject: [PATCH 9/9] fix R5000 hanging the IRIX boot: tell the PROM it has no L2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting R5000 and starting a machine crawled to ~1.4 MIPS and never got past "Starting up the system...". R4400 was fine. The MC EEPROM's CACHSZ word (0x11) is the secondary cache size in 4 KB pages, and the PROM reads it whenever Config.SC says no L2 was probed — which is exactly the R5000 case, since an Indy R5000 board has none. A fresh pages, and the PROM reads it whenever Config.SC says no L2 was probed — which is exactly the R5000 case, since an Indy R5000 board has none. A fresh Eeprom93c56 is *erased*, not zeroed, so that word reads 0xFFFF: 65535 pages, 256 MB of secondary cache. IRIX believes it and spends the boot managing a cache that does not exist. The old code wrote 0 there under `#[cfg(all(feature = "r5k", not(feature = "r5ksc")))]`. That was correct while the CPU was a compile-time choice. Once the model became runtime config, a default build selecting r5000 never compiled that line in, so nothing wrote the word. The fix keys it on the configured CPU instead of on a cargo feature, and derives "has an L2" from the model rather than restating it. R4400 is untouched: it has an L2, Config.SC says so, the PROM probes it and never consults the EEPROM. before R5000 1.4 MIPS, stalls at "Starting up the system..." after R5000 35.5 MIPS, reaches "The system is coming up." R4400 44.7 MIPS, unchanged pre-refactor --features r5k, for reference: 34.9 MIPS Two tests, because nothing caught this. The lib suite passes, the bench is 40/40, and cpu-tests actually improved (2 failures against the old r5k build's 61) — none of them boot an operating system, and this only bites when one manages caches. So: assert an R5000 model reports no secondary cache, and assert a fresh EEPROM reads 0xFFFF rather than 0, which is the trap itself. Neither would have caught the bug alone; together they pin the two facts whose interaction caused it. Also adds Eeprom93c56::cachsz() to make the word readable, and drops a stale reference to the removed SelectedCache alias in a dead r5ksc branch. Unrelated, folded in: the left status block now names the CPU ("Machine: indy · SGI Indy (IP24) · MIPS R4400"), showing the running one while booted and flagging a pending change. --- iris-gui/src/main.rs | 15 ++++++++++++++- src/eeprom_93c56.rs | 18 ++++++++++++++++++ src/machine.rs | 23 ++++++++++++++--------- src/mips_cache_v2.rs | 11 +++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index ff3fab5..6ae24ac 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -3076,10 +3076,23 @@ impl App { ui.separator(); let name = self.prefs.active_machine.as_deref().unwrap_or("(unsaved)"); let profile = self.cfg.machine.profile.label(); + // While running, name the CPU the guest actually booted on — the config + // may already have been changed to one that only takes effect next Start. + let running_cpu = if self.emu.is_running() { self.started_cpu } else { None }; + let cpu = running_cpu.unwrap_or(self.cfg.machine.cpu); ui.label(format!( - "Machine: {name} · {profile}{}", + "Machine: {name} · {profile} · {}{}", + cpu.label(), if self.cfg_dirty { " *" } else { "" } )); + if let Some(started) = running_cpu { + if started != self.cfg.machine.cpu { + ui.label( + RichText::new(format!("{} pending (Stop to apply)", self.cfg.machine.cpu.label())) + .color(Color32::YELLOW).small(), + ); + } + } ui.label(format!("Dirty COW: {}", self.emu.status.dirty_cow)); if let Some((msg, when)) = self.toast.clone() { if when.elapsed().as_secs() < 5 { diff --git a/src/eeprom_93c56.rs b/src/eeprom_93c56.rs index 5294b1c..7e5dfa0 100644 --- a/src/eeprom_93c56.rs +++ b/src/eeprom_93c56.rs @@ -326,6 +326,11 @@ impl Eeprom93c56 { } } + /// Read the secondary cache size register (CACHSZ_REG = word 0x11). + pub fn cachsz(&self) -> u16 { + self.data[0x11] + } + /// Set the secondary cache size register (CACHSZ_REG = word 0x11). /// `pages` = L2 size in 4KB pages (e.g. 256 for 1MB, 128 for 512KB). /// Used by R5K/R4600SC/R4700 PROM to determine secondary cache size. @@ -597,4 +602,17 @@ mod tests { assert!(!patched, "backdoor must not overwrite a non-blank eaddr slot"); assert_eq!(eeprom.get_data()[0x7D..=0x7F], [0x0800, 0x69de, 0xad01]); } + /// The trap behind the R5000 boot hang: a fresh EEPROM is *erased*, not zeroed, + /// so CACHSZ reads as 0xFFFF — 65535 pages, i.e. 256 MB of secondary cache. A + /// machine whose CPU has no L2 must write 0 explicitly; leaving the default is + /// not "no cache", it is "an impossibly large cache". + #[test] + fn a_fresh_eeprom_claims_an_absurd_secondary_cache_until_told_otherwise() { + let mut e = Eeprom93c56::new(); + assert_eq!(e.cachsz(), 0xFFFF, "erased state, not zero"); + assert!(e.cachsz() as u32 * 4096 > 64 * 1024 * 1024, "and it is not a plausible size"); + e.set_cachsz(0); + assert_eq!(e.cachsz(), 0, "a model with no L2 must say so explicitly"); + } + } diff --git a/src/machine.rs b/src/machine.rs index 5f6abc7..e26c25f 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -267,15 +267,20 @@ impl Machine { // 0x1fbb0008 — NVRAM/env vars/MAC, see Eeprom93c56::backdoor_set_mac). let eeprom_mc = Arc::new(Mutex::new(Eeprom93c56::new())); let eeprom_hpc3 = Arc::new(Mutex::new(Eeprom93c56::with_path(crate::devlog::LogModule::Nveeprom, cfg.nveeprom.clone()))); - // CACHSZ_REG (word 0x11): secondary cache size in 4KB pages. - // PROM reads this when SC=1 (size_2nd_cache probe returns 0) to determine L2 size. - // r5ksc without r5ksc_triton: external SC sized via EEPROM. 256 = 1MB (256 × 4KB). - // r5ksc_triton: Triton reports L2 size via CONFIG_TR_SS — EEPROM word left 0. - // r5k without r5ksc: no L2 — leave 0 so PROM sees no secondary cache. - #[cfg(all(feature = "r5ksc", not(feature = "r5ksc_triton")))] - eeprom_mc.lock().set_cachsz((::L2_SIZE / 4096) as u16); - #[cfg(all(feature = "r5k", not(feature = "r5ksc")))] - eeprom_mc.lock().set_cachsz(0); + // CACHSZ_REG (word 0x11): secondary cache size in 4KB pages. The PROM reads + // it when Config.SC reports no probed L2, which is exactly the case for a + // model that has no secondary cache. The EEPROM powers up erased (0xFFFF), + // so such a model MUST write 0 here — otherwise the PROM believes in a + // 256 MB L2 and IRIX spends the boot flushing a cache that does not exist. + // Keyed on the configured CPU, not on a cargo feature: the model is a + // runtime choice, so a feature gate here silently stopped firing. + let model_has_l2 = match cfg_cpu_model { + crate::config::CpuModel::R4400 => ::L2_SIZE > 0, + crate::config::CpuModel::R5000 => ::L2_SIZE > 0, + }; + if !model_has_l2 { + eeprom_mc.lock().set_cachsz(0); + } // 1. Create all devices first // Memory Controller diff --git a/src/mips_cache_v2.rs b/src/mips_cache_v2.rs index 7319e0c..b0c9dc8 100644 --- a/src/mips_cache_v2.rs +++ b/src/mips_cache_v2.rs @@ -3639,4 +3639,15 @@ mod tests { assert!(!t.dirty); assert_eq!(t.cs as u32, L1D_CS_CLEAN_EXCLUSIVE); } + /// A CPU model with no secondary cache must be distinguishable from one that + /// has it, because the PROM decides whether to trust the EEPROM's CACHSZ word + /// on exactly that. Getting this wrong does not fail any unit test or either + /// bare-metal suite — it fails when IRIX boots and starts managing a cache + /// that is not there. See Machine::new's CACHSZ handling. + #[test] + fn only_the_model_with_a_secondary_cache_reports_one() { + assert!(::L2_SIZE > 0, "R4400 has a 1 MB L2"); + assert_eq!(::L2_SIZE, 0, "R5000 (Indy) has no secondary cache"); + } + }