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/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/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/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" 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/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/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index db42673..c4070a9 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 @@ -189,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); @@ -204,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())); @@ -242,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)); @@ -293,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( @@ -357,19 +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"); - ui.label(RichText::new(build_features::CPU).strong()); - ui.end_row(); - }); - 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()); - ui.separator(); - ui.heading("Memory"); if mem_ctx.running { ui.label( @@ -1334,8 +1367,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/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 055b66d..6ae24ac 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) @@ -1202,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)); @@ -2215,6 +2247,7 @@ impl App { MemoryUiContext { running: self.emu.is_running(), started_banks: self.started_banks, + started_cpu: self.started_cpu, }, &mut self.bench, ); @@ -2951,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) @@ -3031,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/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. diff --git a/src/config.rs b/src/config.rs index b7eca5e..1d437f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -510,17 +510,39 @@ 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, 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] + 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() } } } @@ -1310,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")] @@ -1404,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/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/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/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. /// diff --git a/src/machine.rs b/src/machine.rs index a182d1e..e26c25f 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -26,7 +26,7 @@ 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; +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; @@ -60,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, @@ -248,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!( @@ -266,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((crate::mips_cache_v2::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 @@ -695,11 +701,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 { @@ -725,15 +733,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 @@ -742,10 +745,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()); @@ -833,7 +846,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(), )); @@ -1088,7 +1101,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 @@ -1619,6 +1632,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; @@ -1810,6 +1825,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/mips_cache_v2.rs b/src/mips_cache_v2.rs index 7bd337d..b0c9dc8 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,35 @@ 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 { + /// 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; + 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 +450,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 +459,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 +478,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 +619,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 +631,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 +654,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 +799,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 +831,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 +854,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 +977,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 +1005,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 +1034,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 +1056,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 +1075,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 +1098,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 +1113,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 +1126,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 +1133,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 +1159,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 +1195,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 +1203,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 +1219,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 +1230,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 +1250,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 +1266,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 +1291,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 +1303,7 @@ impl R4000Cache { } } } + } // Finally invalidate the L2 line itself self.l2.set_tag(idx, L2Tag::default()); @@ -1283,7 +1334,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 +1365,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 +1384,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 +1397,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 +1423,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 +1438,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 +1455,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 +1467,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 +1479,7 @@ impl R4000Cache { { prev_s1 = Some(idx1); } } } + } // Mark L2 line as dirty let new_cs = match l2_tag.cs() { @@ -1444,7 +1498,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 +1512,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 +1520,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 +1548,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 +1561,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 +1586,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 +1609,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 +1629,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 +1656,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 +1682,7 @@ impl R4000Cache { fuse_pair!(l2_instrs, prev_s1, i, idx0, idx1, r0, r1); } } + } } // Set tag with CleanExclusive state @@ -1648,7 +1702,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 +1718,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 +1736,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 +1758,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 +1781,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 +1799,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 +1810,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 +1818,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 +1834,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 +1845,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 +1863,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 +1884,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 +1937,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 +1948,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 +1994,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 +2006,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 +2028,130 @@ 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 { + 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; + 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 +2166,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 +2175,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 +2194,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 +2226,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 +2241,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 +2269,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 +2281,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)); @@ -2244,18 +2323,15 @@ impl MipsCache for R4000Cache { // L1I and L1D are virtually indexed; L2 is physically indexed. // R5K: for L1 index ops, virt_addr bit 14 selects the way — fold into eidx. + // 2-way encodes the way into the index; direct-mapped is the plain set. let idx = if is_l2 { self.l2.get_index(phys_addr) } else if is_icache { - #[cfg(not(feature = "r5k"))] - { 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) } + 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 { - #[cfg(not(feature = "r5k"))] - { 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) } + 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")] @@ -2268,10 +2344,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 +2356,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 +2571,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 +2585,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 +2612,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 +2641,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 +2705,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 +2723,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 +2752,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 +2778,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 +2791,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 +2805,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 +2827,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 +2845,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 +2868,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 +2899,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 +2932,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 +2947,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 +2979,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 +3000,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 +3012,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 +3048,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 +3069,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 +3098,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 +3120,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 +3134,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 +3156,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 +3190,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 +3200,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 +3209,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 +3229,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 +3252,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 +3272,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 +3302,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 +3322,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 +3331,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 +3358,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 +3374,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 +3391,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 +3413,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 +3422,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 +3452,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 +3469,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 +3480,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 +3552,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. @@ -3554,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"); + } + } 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..5fda05e 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,14 @@ impl MipsExecutor { } core.cp0_config = config; - core.tlb_entries = cfg.tlb_entries as u32; + 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")] @@ -7480,7 +7481,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 +7638,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 +7654,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 +7666,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 +7858,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 +7888,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 +7914,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 +7959,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 +8016,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 +8057,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 +8102,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 +8649,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 +8677,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 +9322,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 +11485,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 +11698,82 @@ impl MipsExecutor { // Resettable + Saveable for MipsCpu (CPU core + TLB) // ============================================================================ -impl Resettable for MipsCpu { +/// 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; + #[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; + 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) } + #[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) + } + 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(); exec.core.reset(false); @@ -11745,7 +11792,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 +11951,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 +11968,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..a38588c 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(); @@ -186,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) @@ -2400,9 +2438,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 +2594,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 +2802,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 +2835,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 +2854,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 +2909,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 +2941,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 +2998,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 +3050,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 +3104,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 +3360,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 +3368,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 +3752,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}; 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 } }); } 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]