Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/suites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions cpu-tests/run/matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 9 additions & 12 deletions installer/iris-gui.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions iris-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 26 additions & 3 deletions iris-gui/src/bench_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub struct BenchState {
/// worker is gone.
last: Option<Result<Box<Run>, 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<ReferenceTable>,
dev: DevRunner,
Expand All @@ -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(),
}
Expand Down Expand Up @@ -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()));
Expand All @@ -108,6 +111,7 @@ impl BenchState {

let opts = BenchOptions {
quick,
cpu,
label: iris::bench_report::cpu_model(),
cancel: Some(cancel.clone()),
..Default::default()
Expand Down Expand Up @@ -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(
Expand Down
73 changes: 54 additions & 19 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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<CpuModel>,
}

/// A host network interface candidate for the PCAP backend selector. This is a
Expand Down Expand Up @@ -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);
Expand All @@ -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()));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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" });
Expand Down
15 changes: 14 additions & 1 deletion iris-gui/src/dialogs/new_machine.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading