Cpu switcher rebased - #91
Merged
Merged
Conversation
R4400 vs R5000 was 99 `#[cfg(feature = "r5k")]` sites, 88 of them inside
mips_cache_v2.rs, plus 54 `mips4` sites gating the ISA. That forced a whole
rebuild per CPU — cpu-tests/run/matrix.sh and `iris-bench matrix` each shell
out to cargo once per cell — and it kept two models from ever being compared
in one process.
The geometry is now const-generic and each model is a type alias:
pub type R4400Cache = CpuCache<16384,16,1,1024, ..., false,0x0440,0x0500,48>;
pub type R5000Cache = CpuCache<32768,32,2,1024, ..., true, 0x2321,0x2300,48>;
Every `#[cfg]` on the CPU axis becomes `if Self::IS_R5K` / `if C::MIPS4` on a
const, so it folds at monomorphisation rather than costing a branch. Verified
in the disassembly: an L1 index is `shr $4; and $1023` on R4400 and
`shr $5; and $511` on R5000, with the discriminator absent from both — the
same code the cfg build emitted.
Model identity that is not cache behaviour lives on its own trait, so
MipsCache stays about caching:
pub trait CpuModel: MipsCache {
const MIPS4: bool; const PRID: u32; const FIR: u32;
const TLB_ENTRIES: usize; const NAME: &'static str;
}
That retires the PRId/FIR cfgs in MipsCore::power_on and the CP0 Config
geometry constants in mips_exec.rs, both of which now come from the model.
mips_core.rs and mips_exec.rs are at zero CPU-feature cfgs.
Tests 407 -> 419 on a default build. The increase is coverage, not inflation:
the L1 stress tests are generic over the model and run against both, and the
10 MIPS IV tests select a MIPS IV model (PassthroughCacheM4) instead of
needing --features mips4, so MIPS IV correctness is checked in every build.
No throughput regression. Interleaved A/B of baseline vs this on the same
machine state, 3 rounds each: +0.73% mean on r4400-jitv2, inside a 4.5%
run-to-run spread, 40/40 accuracy throughout. r4400-interp is +3.7% against a
0.2% noise floor.
Machine still selects the model by cargo feature; making that a runtime
choice is the next step and is why the concrete type appears in exactly one
place there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`[machine] cpu = "r4400" | "r5000"` picks the model from config, beside the
existing `profile`. One binary now runs either CPU:
same default build, no cargo feature
cpu = "r4400" -> guest reports R4400, 47.76 MIPS, 40/40
cpu = "r5000" -> guest reports R5000, 42.19 MIPS, 40/40
which matches what the per-cell compile-time builds measured (~50 / ~43).
Machine holds `Arc<dyn CpuDevice>` and chooses the monomorphisation once, at
construction:
let cpu: Arc<dyn CpuDevice> = match cfg_cpu_model {
CpuModel::R4400 => build_cpu!(R4400Cache),
CpuModel::R5000 => build_cpu!(R5000Cache),
};
The CPU thread runs inside the chosen type, so the vtable is only ever on
setup, control and debug calls — never on the instruction path. CpuDevice is
22 such methods; the two awkward ones were the L1-I counters (now a
MipsCache::set_l1i_counters default method rather than a public field poke)
and the gdb adapter, which needs `self: Arc<Self>` to erase.
Fixes a real ordering bug found by running it rather than by the suite:
MipsCore::new calls reset_registers before the model is known, so setting
only `reset_prid`/`reset_fir` left the live CP0 PRId and CP1 FIR at their
R4400 defaults. An R5000 machine had R5000 cache geometry — visible in the
throughput split — but told the guest it was an R4400. The constructor now
sets both the reset value and the live register.
cpu_model_identity_is_live_at_construction covers it; verified failing with
the fix reverted.
Tests 419 -> 420 default, 418 -> 419 with r5k. The r5k cargo feature is now
vestigial for model selection and can be retired separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CPU became a runtime choice in the previous commit, which turned a
Cargo-flag footgun into a two-clicks-in-the-GUI one: an R4400 snapshot
restored onto an R5000 machine would quietly proceed, even though PRId, FIR
and the whole cache geometry differ and the captured state assumes the
model it was taken on.
The provenance framework was already there — build features and disk
identity are hard errors on restore, paths and nvram are warnings, and
IRIS_SNAPSHOT_SKIP_CHECK=1 downgrades the lot. Two things were missing.
`cpu_model` is a new manifest field, because the model is config now and so
is not covered by the feature list at all. Absent means a legacy manifest,
written when the model really was a build flag, and those still restore —
there is nothing recorded to compare against.
`enabled_features()` also omitted jitv2, jitv2_opcodefusion, opcodefusion and
idle-pause, all of which change execution semantics the captured state
depends on. They are recorded now. A snapshot taken by a jitv2 build before
this commit recorded an incomplete list and will be flagged against a jitv2
build after it; that is the check doing its job, and the existing skip
env var covers anyone who needs the old one back.
The comparison is a free function rather than eight lines inline in
Machine::load_snapshot, so the decision is unit-testable without standing up
a machine. Verified end to end on a real emulator as well:
save on cpu = "r4400" -> manifest carries cpu_model = "R4400"
load on cpu = "r5000" -> Error: snapshot provenance mismatch:
- CPU model differs: snapshot R4400 vs current R5000
— set [machine] cpu = "r4400"
load on cpu = "r4400" -> Snapshot loaded from saves/cputest
Tests 420 -> 422.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Memory tab showed a read-only build constant and said the CPU "is fixed at build time ... To use a different CPU, download that build." That stopped being true at 96e5ddd: both cache models are monomorphised into every binary and Machine::new picks between them from cfg.machine.cpu. The config field, its label() and its ALL list were all already there — nothing surfaced them. So the Memory tab gets a combo box, gated on the machine being stopped and showing running-vs-pending the way the RAM controls already do, and the helper text now describes what actually differs: 16 KB direct-mapped and MIPS III against 2-way 32 KB, no secondary cache and MIPS IV. IRIX reads PRId and configures itself from it, so this is a different machine to the guest rather than a speed knob. Verified end to end rather than by reading the code, using the machine inventory the benchmark suite now prints — one binary, config alone: default R4400 rev 4.0 PRId 0x00000440 16 KB/16 B L2 present cpu = r5000 R5000 rev 2.1 PRId 0x00002321 32 KB/32 B L2 absent **And a bug that fell out of it.** Nothing but the config selects the model, so `--features r5k` no longer produces an R5000 — but that is exactly how both CI matrices and cpu-tests/run/matrix.sh were still choosing one. Their r5000 cells were building a separate binary to get a machine identical to the r4400 cell's, and their own guards ("ran cpu=R4400, expected cpu=R5000") would have caught it on the next push. 96e5ddd changed no CI file and no run config; this does. Adds `--cpu r4400|r5000` so a runner can pick without a second config file, and uses it in suites.yml (both suites) and matrix.sh. Checked that iris-bench forwards it: `run --iris … -- --cpu r5000` produces an R5000 guest at 40/40. Also removes build_features::CPU. It had no callers left once the Memory tab stopped using it, and a constant derived from cargo features can only report how the binary was compiled — which is no longer the same question as which CPU is running, and it was being shown to users as though it were. The r5k cargo feature stays for now: it is what the r5ksc guards key off, and retiring it properly means deciding whether the CI matrix still needs separate r4400/r5000 emulator builds at all. It probably does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CpuDevice's developer-only forwarders called MipsCpu::method(self) meaning
the inherent method, but those six inherent methods are behind
#[cfg(feature = "developer")]. In a build without it the inherent method
does not exist, so the path resolved to the trait method being defined and
the call recursed until the stack ran out.
The six machine.rs wrappers that reach them are already developer-gated, so
the trait declarations and impls now carry the same gates
(set_jitv2_dispatch_enabled needs jitv2 + developer, matching its inherent
counterpart). rustc's unconditional_recursion lint caught this; it was
reachable, not theoretical.
Also cleaned up while here:
- four unnecessary braces left over from turning #[cfg(feature = "r5k")]
blocks into `if Self::IS_R5K`; the L1 index now reads as one binding
with the way OR'd in for the 2-way model instead of two braced arms.
- fetch_update -> try_update in rex3 and jitv2. Renamed upstream for
consistency, stable since 1.95, same semantics and arguments.
- cfg_aliases 0.2.1 -> 0.2.2, which is where the vendored winit build
script's 91 "trailing semicolon in macro" warnings came from. The
requirement is a caret, so a fresh resolve picks it up without a
Cargo.lock change (that file is gitignored).
Both crates are now warning-free: iris across twelve feature combinations
and iris-gui across seven, all with --all-targets. Tests 425 default,
428 developer, 424 r5k.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52c0a23 made the CPU a runtime setting and fixed the CI matrices and cpu-tests/run/matrix.sh, but the benchmark suite still had three ways of getting an R4400 no matter what was asked for. `bench_config` built its MachineConfig with `..Default::default()`, so every in-process run — the GUI's Benchmark tab and `iris-bench run` without --iris — measured an R4400. BenchOptions now carries the model. `iris-bench matrix`'s cells still selected the CPU with `features: "r5k"`, which stopped selecting anything at 96e5ddd. Its r5000 cells were building a second emulator to get a machine identical to the r4400 cell's; the expect_cpu guard would have caught it on the next run. A cell is now a feature set plus a --cpu, and the cached binary is keyed on the feature set, so cells differing only by CPU share one build: r4400-interp (default) r4400 ┐ one binary r5000-interp (default) r5000 ┘ r4400-jitv2 jitv2 r4400 ┐ one binary r5000-jitv2 jitv2 r5000 ┘ Six cells, four builds instead of six. The GUI's Benchmark tab had no CPU control at all; it gets a combo box next to Quick. `iris-bench run` gets --cpu for the in-process path (with --iris, forward it to the emulator: `-- --cpu r5000`). Verified rather than assumed. One binary, in-process: --cpu r4400 -> 51.6 MIPS 100.0% accuracy --cpu r5000 -> 43.7 MIPS 100.0% accuracy and the full matrix now reports the CPU each cell asked for — R4400, R4400, R5000, R5000 — where before every cell said R4400. All four at 40/40, and every delta against the pre-change baseline (+4.2, -3.6, +0.4, -0.2 %) is inside this host's ~4.5% run-to-run noise, measured earlier by re-running identical binaries. Note bench/build/ still holds stale per-cell binaries from the old naming (iris-r4400-interp and friends). Nothing reads them now; they can be deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CPU became a per-machine setting in 9ef7b06, but the only place to see or change it was a combo box on the Memory tab — which is the wrong tab for it, and left three places describing a machine without mentioning which processor it has. - New Machine now offers the CPU next to the machine model, so a machine starts life with the one you wanted instead of being created as an R4400 and edited afterwards. - The left-hand machine summary gains a Processor row, and a "Processor (last Start)" row when the running guest differs from the config — the same shape the RAM rows already use. - Processor moves from Memory to General. It sits with Platform, which is the other thing that decides what machine the guest thinks it is; RAM is a quantity, this is an identity. - The Machine menu gets the selection too, mirroring the Memory menu: radio buttons disabled while running, "Running: X (Stop to apply edits)" in yellow when the config has moved on, and "CPU changes apply after Stop → Start" underneath. Picking one toasts "applies at next Start". R4400 remains the default in every one of them: CpuModel::default(). No CLI work was needed — `iris --cpu r4400|r5000` already exists from 85e44a4 and defaults to r4400. iris-ci drives an already-running emulator over the control socket and never constructs a machine, so it needs no switch of its own: launch the emulator with `iris --ci --cpu r5000` and iris-ci attaches to whatever is running. Checked by launching the GUI, not only by compiling it: it stays up with no panic and no egui id-collision warnings, and the two panels these changes touch are the ones that render on startup (General is the default tab, welcome_panel shows while stopped). The menu and the modal need a click, so those are compile- and id-checked only — every from_id_salt in the crate is unique. No screenshot: this box has no Xvfb or capture tools. Clean across default, appstore, bundled, premiere and pcap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both described the R5000 as something you build or install instead of the R4400, which stopped being true at 9ef7b06. installer/iris-gui.iss carried a whole second product — "IRIS (R5000)" with its own AppId, name and default folder — built by passing /DR5000=1. Nothing passes it any more, and it would now ship a byte-identical exe under a different name, so the variant is gone and there is one "IRIS". README's section was titled after the cargo feature and told you to run `cargo run --release --features r5k`. It is now "Emulated CPU": a table of what actually differs between the two, and the three ways to pick one (GUI, `[machine] cpu`, `--cpu`). Its performance note was wrong in a way worth not repeating. It claimed "roughly 5% lower instruction throughput" for the R5000 as a flat figure. The sign depends on the engine — measured on this tree, bare-metal suite: interp R4400 52.45 R5000 43.35 R5000 -17.4% jitv2 R4400 192.30 R5000 210.78 R5000 +9.6% Slower under the interpreter, where probing both ways is on the hot path; faster under jitv2, where it isn't and the larger cache with 32-byte lines pays off. The text now says both, and says not to compare scores across CPUs. One consequence to be aware of when releasing: anyone who installed the old "IRIS (R5000)" has it under a different AppId, so the single installer will not upgrade or replace it — it will sit alongside as a separate entry until they uninstall it by hand. Silently orphaning it is the same failure the AppId comment in this file already warns about; there is no Inno mechanism to adopt a foreign AppId, so it needs a release note rather than a code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting R5000 and starting a machine crawled to ~1.4 MIPS and never got
past "Starting up the system...". R4400 was fine.
The MC EEPROM's CACHSZ word (0x11) is the secondary cache size in 4 KB
pages, and the PROM reads it whenever Config.SC says no L2 was probed —
which is exactly the R5000 case, since an Indy R5000 board has none. A fresh
pages, and the PROM reads it whenever Config.SC says no L2 was probed —
which is exactly the R5000 case, since an Indy R5000 board has none. A fresh
Eeprom93c56 is *erased*, not zeroed, so that word reads 0xFFFF: 65535 pages,
256 MB of secondary cache. IRIX believes it and spends the boot managing a
cache that does not exist.
The old code wrote 0 there under `#[cfg(all(feature = "r5k", not(feature =
"r5ksc")))]`. That was correct while the CPU was a compile-time choice. Once
the model became runtime config, a default build selecting r5000 never
compiled that line in, so nothing wrote the word. The fix keys it on the
configured CPU instead of on a cargo feature, and derives "has an L2" from
the model rather than restating it.
R4400 is untouched: it has an L2, Config.SC says so, the PROM probes it and
never consults the EEPROM.
before R5000 1.4 MIPS, stalls at "Starting up the system..."
after R5000 35.5 MIPS, reaches "The system is coming up."
R4400 44.7 MIPS, unchanged
pre-refactor --features r5k, for reference: 34.9 MIPS
Two tests, because nothing caught this. The lib suite passes, the bench is
40/40, and cpu-tests actually improved (2 failures against the old r5k
build's 61) — none of them boot an operating system, and this only bites
when one manages caches. So: assert an R5000 model reports no secondary
cache, and assert a fresh EEPROM reads 0xFFFF rather than 0, which is the
trap itself. Neither would have caught the bug alone; together they pin the
two facts whose interaction caused it.
Also adds Eeprom93c56::cachsz() to make the word readable, and drops a stale
reference to the removed SelectedCache alias in a dead r5ksc branch.
Unrelated, folded in: the left status block now names the CPU
("Machine: indy · SGI Indy (IP24) · MIPS R4400"), showing the running one
while booted and flagging a pending change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Switch CPU during runtime
Embedded by value, no indirection:
pub struct MipsExecutor<T: Tlb, C: CpuModel> {
pub tlb: T, // inline
pub cache: C, // inline
}
// and one level down, inside the cache:
ic: ICacheT<IC_SIZE, IC_LINE, IC_WAYS, IC_TAGS>,
dc: DCacheT<DC_SIZE, DC_LINE, DC_WAYS, DC_TAGS, DC_DATA>,
l2: L2CacheT<L2_CACHE_SIZE, L2_LINE, L2_TAGS, L2_DATA, L2_NINSTRS>,
Disassembly of the real MipsCache::read for both monomorphizations:
probe4 (R4400, 16K/16B/1-way) probe5 (R5000, 32K/32B/2-way)
shr $0x4,%r12d shr $0x5,%rcx
and $0x3ff,%r12d and $0x1ff,%r15d
shl $0x4,%ecx shl $0x4,%edi
cmp %r13,(%rax,%rcx,1) cmp %r13,(%rax,%rdi,1)
Literal shift/mask with model-specific constants (>>4 & 1023 vs >>5 & 511), tag compare straight off a fixed
field offset (0x50). Zero model-flag references in either — IS_R5K is gone at compile time. And the R5000-only
LRU way-selection appears as bit-ops in probe5 only (2) and not at all in probe4 (0), which is the direct
evidence that the R5000 path isn't compiled into the R4400 monomorphization.
Where indirection does exist, so it's not overstated:
Machine → Arc — vtable, but above the CPU. Only start/stop/snapshot/debug. The CPU thread
enters run() and loops guard.step() on a concrete type; it never crosses the vtable per instruction.
sysad: Arc — pre-existing, the CPU↔bus edge was already virtual.
Cache's tags/data are Boxed heap arrays — pre-existing.
MipsCpu wraps the executor in Arc<Mutex<…>> — pre-existing; the lock is taken once before the run loop, not
per instruction.