From 45ad90279153fc5fe78a7cc3503e7783f962f1b2 Mon Sep 17 00:00:00 2001 From: iblowmymind <28228415+iblowmymind@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:38:16 +0300 Subject: [PATCH] UI consistency upgrades Updates UI elements, adding controls for recently introduced features (such as IP28 support) --- CHANGELOG.md | 36 +++ iris-gui-README.md | 5 +- iris-gui/Cargo.toml | 18 ++ iris-gui/src/config_ui.rs | 238 ++++++++++++++++-- iris-gui/src/dialogs/new_machine.rs | 71 +++++- iris-gui/src/handle.rs | 23 +- iris-gui/src/main.rs | 19 +- ...s-generic-new-boards-need-no-gui-wiring.md | 27 ++ src/lib.rs | 16 ++ src/machine.rs | 9 + 10 files changed, 431 insertions(+), 31 deletions(-) create mode 100644 rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8702bd12..02132be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,42 @@ is easiest to understand by reading the commit. Extreme (Indigo2 only). Picking a GR2 board resets heads, resolution and `[impact]` to values `validate()` accepts; the Newport heads control is hidden for GR2. +- **IP28 / R10000 and IMPACT graphics wired into the GUI.** A new `ip28` + crate feature (`cargo build -p iris-gui --features ip28`, passes through to + `iris/ip28`) puts the Indigo2 IMPACT (IP28) machine profile and R10000 CPU + in the Machine model / Processor dropdowns, gates the 256 MB RAM bank size + (the IP28 MC's own granule) with a hint everywhere banks are picked, and + reports build status on the Debug tab. The New Machine dialog nudges + Processor to R10000 and turns off "use embedded PROM" when IP28 is picked + (there is no embedded IP28 PROM — only IP22/IP24 — so it needs a real + dumped image), and pre-sets IMPACT graphics so a fresh machine has a + display. +- **The graphics board picker above now also covers IMPACT** (Solid / High / + Maximum, Indigo2 only — either IP22 or IP28; no feature needed, since the + `[impact]` config section and the `mgras` board model it drives already + existed). It's the same dropdown, now unifying `[graphics].board` and + `[impact]` since they claim the same GIO gfx slot: picking a non-Newport + choice resets heads, resolution, and the other of the two config sections + to values `validate()` accepts, and the Newport heads/resolution controls + are hidden whenever either is active. Moving the machine profile off + Indigo2 falls the picker back to Newport instead of leaving a config + `validate()` would reject at Start. +- **CP0 Count clock (Processor section, General tab):** a `[clock] fixed_mhz` + control (was CLI/TOML only) with an "Auto" reset to the profile's default + (33 MHz, or 97.5 MHz on IP28). +- **Kernel Hz next to MIPS in the status footer** (`Machine::fasttick_count`, + new in core): the guest's own clock-tick rate — CP0 Compare matches, or the + IOC's 8254 timer interrupts when IRIX uses those instead — distinct from + the MIPS readout's host emulation throughput. The CLI's baked status bar + has shown this since `837b048`; iris-gui had no equivalent readout at all. +- **Host services / host OpenGL build status on the Debug tab.** New passthrough + `hostcall`/`hostgl` crate features (`iris/hostcall`, `iris/hostgl` — see + `c8dc330`, `1a93808`): private syscalls 3000-3009 let an IRIX program built + against the replacement libGL (iris-guest-tools) ask the host for something + the emulated machine doesn't have, with host OpenGL the first service. There + is no per-machine config for either — a guest either gets the trap or + doesn't — so the GUI's only job is reporting what's built in (and, for + `hostgl`, that only macOS/CGL has a backend so far). - Scaling and resize fixes (`e93c5bb`): the VM screen scale is now the maximum draw scale, so a larger window centres the picture instead of stretching it; a snap-to-size request made while fullscreen is applied when fullscreen ends. diff --git a/iris-gui-README.md b/iris-gui-README.md index 1953b27b..5e76449c 100644 --- a/iris-gui-README.md +++ b/iris-gui-README.md @@ -39,6 +39,9 @@ emulation will be noticeably slow. | `pcap` | PCAP bridged networking; the Networking tab lists host interfaces. Needs libpcap / a WinPcap-compatible SDK | | `daynaport` | DaynaPort SCSI/Link targets on the Disks tab (without it the option is shown with a rebuild hint) | | `ultra64` | N64 development board toggle and help window | +| `ip28` | Indigo2 IMPACT (IP28) machine profile and R10000 CPU in the Machine model / Processor dropdowns. Bring-up status; implies `ppmem`. The IMPACT/MGRAS graphics board itself needs no feature — it's selectable in the Graphics board dropdown for any Indigo2 profile | +| `hostcall` | Host services for IRIX programs over private syscalls 3000-3009. No per-machine setting — the Debug tab just reports it's built in | +| `hostgl` | Host OpenGL for IRIX programs (implies `hostcall`); IRIX's replacement libGL replays its GL calls on the host GPU. macOS (CGL) only for now — builds elsewhere but registers no backend | | `premiere` | `iris/lightning` + `iris/idle-pause` for maximum in-process speed | | `bundled` | Distributed build: hides the iris.toml import/export items. Set by the Release workflow | | `appstore` | Mac App Store build: implies `bundled`, hides the CI tab, enables security-scoped bookmarks and folder grants | @@ -141,7 +144,7 @@ IRIX with a media-change Unit Attention, no restart needed. | Tab | What's there | | --- | --- | -| **General** | Platform (Indy / Indigo2), CPU, Newport heads, display resolution, PROM, NVRAM, ttyd1 serial log | +| **General** | Platform (Indy / Indigo2 / Indigo2 IMPACT with `ip28`), CPU (R4400 / R5000 / R10000 with `ip28`), graphics board (Newport, GR2 XZ/Extreme, or IMPACT Solid/High/Max — one dropdown covers both `[graphics]` and `[impact]`, since they share the GIO gfx slot), Newport heads, display resolution, PROM, NVRAM, ttyd1 serial log | | **Disks** | SCSI devices: image paths, CD-ROM discs, COW overlay, scratch volume, DaynaPort, controller (Indigo2) | | **Networking** | NAT subnet (applied live, with conflict checks against host interfaces), port forwards (added/removed live), NFS share, PCAP interface, **Check networking** diagnostics | | **Memory** | RAM banks and the resulting total | diff --git a/iris-gui/Cargo.toml b/iris-gui/Cargo.toml index 25c61281..476995f7 100644 --- a/iris-gui/Cargo.toml +++ b/iris-gui/Cargo.toml @@ -89,6 +89,24 @@ premiere = ["iris/lightning", "iris/idle-pause"] # `ultra64` field on iris's Config only exists with the core feature on). # Build with: cargo build -p iris-gui --features ultra64 ultra64 = ["iris/ultra64"] +# The Indigo2 IMPACT (IP28) machine profile and its R10000 CPU (bring-up +# status — see the `8579614` commit and `ip28.toml.example`). Implies ppmem. +# Off by default: it's a much bigger memory-model change than the other +# passthrough features. Gates the "SGI Indigo2 IMPACT (IP28)" machine profile +# and "MIPS R10000" CPU dropdown entries; the IMPACT/MGRAS graphics board +# itself (Indigo2 profiles, either IP22 or IP28) needs no feature and is +# always selectable. Build with: cargo build -p iris-gui --features ip28 +ip28 = ["iris/ip28"] +# Host services for IRIX programs over private syscalls 3000-3009 +# (`iris-hostcall`) — no per-machine config, a guest either gets the trap or +# doesn't. Build with: cargo build -p iris-gui --features hostcall +hostcall = ["iris/hostcall"] +# Host OpenGL for IRIX programs (`iris-hostgl`), replaying the guest's GL +# command stream on the host GPU; implies hostcall. Only macOS has a backend +# (CGL) today — elsewhere this builds but registers nothing, and the guest's +# replacement libGL (github.com/atomchild411/iris-guest-tools) reports host +# GL as unavailable. Build with: cargo build -p iris-gui --features hostgl +hostgl = ["hostcall", "iris/hostgl"] [dependencies] # Group A (additive) features are always on for iris-gui so the user can enable diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 7efe9019..a90a01a2 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -2,9 +2,9 @@ use egui::{Color32, ComboBox, DragValue, Grid, RichText, ScrollArea, TextEdit, U use iris::build_features; use std::path::Path; use iris::config::{ - format_unix_utc, CpuModel, ForwardBind, ForwardProto, GraphicsBoard, MachineConfig, - MachineProfile, NetMode, NfsConfig, PortForwardConfig, RtcOffset, ScsiDeviceConfig, ScsiKind, - VinoSource, VinoStandard, VALID_BANK_SIZES, + format_unix_utc, CpuModel, ForwardBind, ForwardProto, GraphicsBoard, ImpactSection, + ImpactSlot, MachineConfig, MachineProfile, NetMode, NfsConfig, PortForwardConfig, RtcOffset, + ScsiDeviceConfig, ScsiKind, VinoSource, VinoStandard, VALID_BANK_SIZES, }; use iris::nfsudp::NfsVersion; use iris::vc2_timings::NewportResolution; @@ -223,6 +223,24 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) if !cfg.graphics.board.supports(cfg.machine.profile) { cfg.graphics.board = GraphicsBoard::Xz; } + // [impact] (IMPACT/MGRAS graphics) is Indigo2-only, either profile; + // moving to Indy falls back to Newport rather than leaving a config + // `validate()` would reject at Start. + if cfg.impact.any_enabled() + && !matches!(cfg.machine.profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28) + { + cfg.impact = Default::default(); + } + // 256 MB banks are the IP28 MC's granule only; moving away from it would + // leave a config `validate()` rejects at Start. Compared against the + // profile *variant*, not `.ip28()` (which also requires the `ip28` build + // feature) — a build without the feature must not silently rewrite a + // perfectly good IP28 machine's banks just because it can't run it. + if cfg.machine.profile != MachineProfile::Indigo2Ip28 { + for bank in &mut cfg.banks { + if *bank == 256 { *bank = 128; } + } + } ui.label( RichText::new( "IRIX Software Manager and hinv report IP22 as the platform family on Indy — that is normal. \ @@ -249,6 +267,18 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) .weak() .small(), ); + } + if cfg.machine.profile == MachineProfile::Indigo2Ip28 { + ui.label( + RichText::new( + "Bring-up: an R10000 module in the Indigo2 chassis, 16 MB MEMCFG \ + granule (256 MB banks below), RAM at 0x20000000. IRIX carries no \ + Newport driver for this board — set Graphics board to an IMPACT \ + option below, not Newport.", + ) + .color(Color32::from_rgb(220, 170, 90)) + .small(), + ); } ui.heading("Processor"); Grid::new("cpu_grid").num_columns(2).striped(true).show(ui, |ui| { @@ -267,6 +297,21 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) }); }); ui.end_row(); + + ui.label("CP0 Count clock"); + ui.horizontal(|ui| { + let auto_default = if cfg.machine.profile.ip28() { 97.5 } else { 33.0 }; + let mut mhz = cfg.clock.fixed_mhz.unwrap_or(auto_default); + let changed = ui.add(DragValue::new(&mut mhz).range(0.1..=1000.0).suffix(" MHz")).changed(); + if changed { cfg.clock.fixed_mhz = Some(mhz); } + if ui.add_enabled(cfg.clock.fixed_mhz.is_some(), egui::Button::new("Auto")) + .on_hover_text(format!("Default: {auto_default} MHz — what IRIX reads as CPU speed")) + .clicked() + { + cfg.clock.fixed_mhz = None; + } + }); + ui.end_row(); }); if mem_ctx.running { @@ -290,10 +335,18 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) reads the CPU from PRId and configures itself accordingly, so switching is a \ different machine to the guest, not a speed knob.") .weak().small()); + if cfg.machine.cpu == CpuModel::R10000 { + ui.label(RichText::new( + "The R10000 is a shadow cache (loads/stores go straight to memory; the \ + tag/data arrays only answer CACHE ops and PROM diagnostics), a 64-entry \ + JTLB, and 44-bit virtual addresses. Bring-up status — pair it with the \ + IP28 machine profile.") + .weak().small()); + } ui.separator(); show_board_picker(ui, cfg, mem_ctx.running); - if cfg.graphics.board == GraphicsBoard::Newport { + if cfg.graphics.board == GraphicsBoard::Newport && !cfg.impact.any_enabled() { ui.horizontal(|ui| { ui.label("Newport heads"); ui.add(egui::DragValue::new(&mut cfg.graphics.heads).range(1..=2).speed(0.1)); @@ -337,6 +390,18 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) path_row_opt(ui, "serial_log", &mut cfg.serial_log, Pick::SaveFile, ANY_FILTERS); ui.end_row(); }); + if cfg.machine.profile.ip28() && !crate::disk_readable(&cfg.prom) { + ui.label( + RichText::new( + "There is no embedded IP28 PROM — this PROM path doesn't resolve to a \ + readable file, and falling back to the built-in IP22 PROM will not pass \ + POST on an IP28. Point PROM image at a real dumped IP28 PROM \ + (see ip28.toml.example).", + ) + .color(Color32::from_rgb(220, 170, 90)) + .small(), + ); + } show_rtc_offset(ui, &mut cfg.rtc_offset, mem_ctx.running); @@ -435,29 +500,109 @@ fn show_rtc_offset(ui: &mut Ui, off: &mut RtcOffset, running: bool) { ); } +/// The graphics board picker unifies two independent config fields +/// (`graphics.board` and `[impact]`) into one dropdown — they claim the same +/// GIO gfx slot and `validate()` refuses a config that sets both. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GfxChoice { + Newport, + Xz, + Extreme, + ImpactSolid, + ImpactHigh, + ImpactMax, +} + +impl GfxChoice { + const ALL: [Self; 6] = [ + Self::Newport, Self::Xz, Self::Extreme, + Self::ImpactSolid, Self::ImpactHigh, Self::ImpactMax, + ]; + + fn from_cfg(cfg: &MachineConfig) -> Self { + match cfg.impact.gfx { + ImpactSlot::Solid => Self::ImpactSolid, + ImpactSlot::High => Self::ImpactHigh, + ImpactSlot::Max => Self::ImpactMax, + ImpactSlot::None => match cfg.graphics.board { + GraphicsBoard::Newport => Self::Newport, + GraphicsBoard::Xz => Self::Xz, + GraphicsBoard::Extreme => Self::Extreme, + }, + } + } + + fn label(self) -> &'static str { + match self { + Self::Newport => "Newport (XL)", + Self::Xz => "GR2 XZ", + Self::Extreme => "GR2 Extreme", + Self::ImpactSolid => "IMPACT Solid", + Self::ImpactHigh => "IMPACT High", + Self::ImpactMax => "IMPACT Maximum", + } + } + + /// Whether `validate()` accepts this choice on `profile`. + fn supports(self, profile: MachineProfile) -> bool { + match self { + Self::Newport | Self::Xz => true, + Self::Extreme => profile == MachineProfile::Indigo2Ip22, + Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax => { + matches!(profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28) + } + } + } + + fn is_impact(self) -> bool { + matches!(self, Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax) + } + + fn apply(self, cfg: &mut MachineConfig) { + cfg.graphics.board = match self { + Self::Xz => GraphicsBoard::Xz, + Self::Extreme => GraphicsBoard::Extreme, + Self::Newport | Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax => GraphicsBoard::Newport, + }; + cfg.impact = ImpactSection { + gfx: match self { + Self::ImpactSolid => ImpactSlot::Solid, + Self::ImpactHigh => ImpactSlot::High, + Self::ImpactMax => ImpactSlot::Max, + Self::Newport | Self::Xz | Self::Extreme => ImpactSlot::None, + }, + ..Default::default() + }; + } +} + fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let before = cfg.graphics.board; + let before = GfxChoice::from_cfg(cfg); + let mut choice = before; ui.horizontal(|ui| { ui.label("Graphics board"); ui.add_enabled_ui(!running, |ui| { ComboBox::from_id_salt("graphics_board") - .selected_text(cfg.graphics.board.label()) + .selected_text(choice.label()) .show_ui(ui, |ui| { - for b in GraphicsBoard::ALL { - ui.add_enabled_ui(b.supports(cfg.machine.profile), |ui| { - ui.selectable_value(&mut cfg.graphics.board, b, b.label()) + for c in GfxChoice::ALL { + ui.add_enabled_ui(c.supports(cfg.machine.profile), |ui| { + ui.selectable_value(&mut choice, c, c.label()) .on_disabled_hover_text("Indigo2 only"); }); } }); }); }); - // GR2 is single-head, has no VC2 presets and shares the gfx slot with - // IMPACT; clear those so the config still passes `validate()`. - if cfg.graphics.board != before && cfg.graphics.board != GraphicsBoard::Newport { - cfg.graphics.heads = 1; - cfg.graphics.resolution = NewportResolution::Guest; - cfg.impact = Default::default(); + // Every non-Newport choice is single-head with no VC2 presets, and GR2 / + // IMPACT share the gfx slot; clear those so the config still passes + // `validate()`. + if choice != before { + choice.apply(cfg); + if choice != GfxChoice::Newport { + cfg.graphics.heads = 1; + cfg.graphics.resolution = NewportResolution::Guest; + } } if running { ui.label( @@ -466,7 +611,7 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { .small(), ); } - if cfg.graphics.board != GraphicsBoard::Newport { + if matches!(choice, GfxChoice::Xz | GfxChoice::Extreme) { ui.label( RichText::new( "GR2 is newer than Newport: PROM, textport, X and GL work, with gaps. \ @@ -476,10 +621,27 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { .small(), ); } + if choice.is_impact() { + ui.label( + RichText::new(if cfg.machine.profile == MachineProfile::Indigo2Ip28 { + "IMPACT (MGRAS): PROM POST, textport and the 4Dwm desktop work on IP28 — the \ + raster engine covers lines, rects, fills, RGB/CI pixels and overlay planes. \ + TRAM and 3D GL are not modeled. No Newport fallback — the IP28 IRIX kernel \ + carries no Newport driver at all. Single head, guest-programmed resolution." + } else { + "IMPACT (MGRAS): a register-level model — lines, rects, fills, RGB/CI pixels \ + and overlay planes. TRAM and 3D GL are not modeled. Confirmed booting to the \ + 4Dwm desktop on the IP28 machine profile; untested standalone on IP22. Single \ + head, guest-programmed resolution." + }) + .weak() + .small(), + ); + } } fn show_resolution_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let newport = cfg.graphics.board == GraphicsBoard::Newport && !cfg.headless; + let newport = cfg.graphics.board == GraphicsBoard::Newport && !cfg.headless && !cfg.impact.any_enabled(); ui.horizontal(|ui| { ui.label("Display resolution"); if !newport { @@ -546,6 +708,7 @@ fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) { .small(), ); } + let ip28_banks = cfg.machine.profile.ip28(); ui.add_space(4.0); ui.label("Quick presets:"); ui.horizontal_wrapped(|ui| { @@ -555,11 +718,15 @@ fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) { .on_disabled_hover_text("Stop the VM to change RAM") .clicked() { - cfg.banks = crate::dialogs::new_machine::distribute_ram(p); + cfg.banks = crate::dialogs::new_machine::distribute_ram(p, ip28_banks); } } }); - ui.label("RAM bank sizes in MB (valid: 0, 8, 16, 32, 64, 128)"); + ui.label(if ip28_banks { + "RAM bank sizes in MB (valid: 0, 8, 16, 32, 64, 128, 256 — the IP28 MC only)" + } else { + "RAM bank sizes in MB (valid: 0, 8, 16, 32, 64, 128 — 256 needs the IP28 machine profile)" + }); Grid::new("mem_grid").num_columns(2).striped(true).show(ui, |ui| { for i in 0..4 { ui.label(format!("Bank {i}")); @@ -568,7 +735,10 @@ fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) { ComboBox::from_id_salt(("bank", i)).selected_text(format!("{cur} MB")) .show_ui(ui, |ui| { for &sz in VALID_BANK_SIZES { - ui.selectable_value(&mut cfg.banks[i], sz, format!("{sz} MB")); + ui.add_enabled_ui(sz != 256 || ip28_banks, |ui| { + ui.selectable_value(&mut cfg.banks[i], sz, format!("{sz} MB")) + .on_disabled_hover_text("256 MB banks need the IP28 machine profile"); + }); } }); }); @@ -1504,7 +1674,35 @@ fn show_debug(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { build_features::CAMERA, )); ui.end_row(); + ui.label("IP28 / R10000"); + ui.label(if build_features::IP28 { + "built in — Indigo2 IMPACT (IP28) profile and R10000 CPU selectable" + } else { + "not built — rebuild with --features ip28 for the IP28 profile / R10000 CPU" + }); + ui.end_row(); + ui.label("Host services / Host GL"); + ui.label(match (build_features::HOSTCALL, build_features::HOSTGL) { + (_, true) => "built in — IRIX OpenGL programs render on the host GPU (CGL)", + (true, false) if cfg!(target_os = "macos") => + "hostcall only — rebuild with --features hostgl for host OpenGL", + (true, false) => + "hostcall only — host GL has no backend on this OS yet (macOS/CGL only)", + (false, _) => "not built — rebuild with --features hostgl for host OpenGL", + }); + ui.end_row(); }); + if build_features::HOSTCALL { + ui.label( + RichText::new( + "Host services answer private syscalls 3000-3009 from IRIX programs built \ + against the replacement libGL (github.com/atomchild411/iris-guest-tools) — \ + no per-machine setting, a guest either gets the trap or doesn't.", + ) + .weak() + .small(), + ); + } ui.label( RichText::new( "Status-bar MIPS = host emulation throughput. IRIX System Manager MHz \ diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index c78de473..d40a462f 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -56,13 +56,18 @@ impl Default for NewMachineDialog { } } -pub fn distribute_ram(total: u32) -> [u32; 4] { - // Greedy fill banks 0..3 with the largest valid bank size that fits. +/// Greedy-fill banks 0..3 with the largest valid bank size that fits. +/// `allow_256` gates the 256 MB size, which only the IP28 MC can express +/// (`validate()` rejects it on any other machine profile) — pass +/// `cfg.machine.profile.ip28()`. +pub fn distribute_ram(total: u32, allow_256: bool) -> [u32; 4] { let mut remaining = total; let mut banks = [0u32; 4]; for slot in &mut banks { // Pick the largest size in VALID_BANK_SIZES that is <= remaining. - let pick = VALID_BANK_SIZES.iter().filter(|&&s| s > 0 && s <= remaining).max().copied().unwrap_or(0); + let pick = VALID_BANK_SIZES.iter() + .filter(|&&s| s > 0 && s <= remaining && (s != 256 || allow_256)) + .max().copied().unwrap_or(0); *slot = pick; remaining -= pick; if remaining == 0 { break; } @@ -92,6 +97,7 @@ impl NewMachineDialog { ui.end_row(); ui.label("Machine model"); + let profile_before = self.profile; ComboBox::from_id_salt("nm_profile") .selected_text(self.profile.label()) .show_ui(ui, |ui| { @@ -99,6 +105,29 @@ impl NewMachineDialog { ui.selectable_value(&mut self.profile, p, p.label()); } }); + if self.profile != profile_before { + // The IP28 machine means "R10000 in the Indigo2 chassis" — + // nudge the Processor pick to match; leaving it un-does the + // nudge rather than stranding the user on R10000 elsewhere. + if self.profile == MachineProfile::Indigo2Ip28 { + self.cpu = CpuModel::R10000; + self.use_embedded_prom = false; + // There is no embedded IP28 PROM (only IP22/IP24) — clear the + // IP22/IP24 placeholder path so Create can't silently ship a + // machine that falls back to the wrong embedded PROM. + self.prom_path.clear(); + // Newport resolution presets don't apply to IMPACT. + self.resolution = NewportResolution::Guest; + } else if profile_before == MachineProfile::Indigo2Ip28 { + self.cpu = CpuModel::default(); + self.use_embedded_prom = true; + if self.prom_path.is_empty() { self.prom_path = "prom.bin".into(); } + // 256 MB banks are the IP28 MC's granule only. + for bank in &mut self.ram_banks { + if *bank == 256 { *bank = 128; } + } + } + } ui.end_row(); ui.label("Processor"); @@ -168,13 +197,17 @@ impl NewMachineDialog { }); ui.end_row(); } else { + let ip28_banks = self.profile.ip28(); for i in 0..4 { ui.label(format!("Bank {i}")); ComboBox::from_id_salt(("nm_bank", i)) .selected_text(format!("{} MB", self.ram_banks[i])) .show_ui(ui, |ui| { for &sz in VALID_BANK_SIZES { - ui.selectable_value(&mut self.ram_banks[i], sz, format!("{sz} MB")); + ui.add_enabled_ui(sz != 256 || ip28_banks, |ui| { + ui.selectable_value(&mut self.ram_banks[i], sz, format!("{sz} MB")) + .on_disabled_hover_text("256 MB banks need the IP28 machine profile"); + }); } }); ui.end_row(); @@ -189,6 +222,27 @@ impl NewMachineDialog { ui.end_row(); }); + if self.profile == MachineProfile::Indigo2Ip28 { + ui.add_space(4.0); + ui.label( + RichText::new( + "Bring-up. The new machine is set up with IMPACT graphics — the \ + IP28 IRIX kernel has no Newport driver at all; change it from the \ + General tab after creation if needed. The embedded PROM is \ + IP22/IP24 only — point PROM image at a real dumped IP28 PROM.", + ) + .color(Color32::from_rgb(220, 170, 90)) + .small(), + ); + if !crate::disk_readable(&self.prom_path) { + ui.label( + RichText::new("PROM image doesn't point at a readable file yet.") + .color(Color32::from_rgb(220, 120, 90)) + .small(), + ); + } + } + ui.separator(); ui.label(RichText::new("Boot disk (optional)").strong()); Grid::new("new_machine_disk").num_columns(2).striped(true).show(ui, |ui| { @@ -257,8 +311,15 @@ impl NewMachineDialog { cfg.banks = if self.ram_advanced { self.ram_banks } else { - distribute_ram(self.ram_total_mb) + distribute_ram(self.ram_total_mb, self.profile.ip28()) }; + // The IP28 IRIX kernel carries no Newport driver at all — + // default straight to IMPACT so a fresh machine has a working + // display instead of a blank Newport head. See config_ui.rs's + // GfxChoice picker, which the user can still change afterwards. + if self.profile == MachineProfile::Indigo2Ip28 { + cfg.impact.gfx = iris::config::ImpactSlot::Solid; + } // SCSI defaults: drop the built-in entries unless the // user explicitly opted in. cfg.scsi.clear(); diff --git a/iris-gui/src/handle.rs b/iris-gui/src/handle.rs index 004643a1..18a78f8a 100644 --- a/iris-gui/src/handle.rs +++ b/iris-gui/src/handle.rs @@ -85,6 +85,10 @@ pub struct Status { pub dirty_cow: usize, /// Approximate instructions/sec (millions). pub mips: f32, + /// The kernel's clock-tick rate in Hz (see `Machine::fasttick_count`) — + /// what IRIX actually ticks time with, distinct from `mips` (host + /// emulation throughput). Mirrors the CLI status bar's Hz readout. + pub kernel_hz: f32, /// The CPU is not executing: either stopped (soft power-off) or idle at the /// PROM after an IRIX `halt` (0 MIPS). When set, the guest has shut down and /// stopping the machine can't corrupt a disk — see [`crate::safe_stop`]. @@ -316,6 +320,10 @@ fn worker_loop( // refresh/status-bar loop. `None` until a machine is up. let mut cycles: Option = None; let mut prev_cycles: u64 = 0; + // Kernel tick-rate Hz, sampled alongside MIPS from the same shared + // counter the CLI status bar reads (see `Machine::fasttick_count`). + let mut fasttick: Option> = None; + let mut prev_fasttick: u64 = 0; let mut prev_tick = std::time::Instant::now(); // Tick cadence for the status poll while idle on the command channel. const STATUS_TICK: std::time::Duration = std::time::Duration::from_millis(500); @@ -331,6 +339,14 @@ fn worker_loop( let dc = cur.wrapping_sub(prev_cycles); let mips = (dc as f64 / dt / 1_000_000.0 * 10.0).round() as f32 / 10.0; prev_cycles = cur; + let kernel_hz = if let Some(ft) = &fasttick { + let cur_ft = ft.load(std::sync::atomic::Ordering::Relaxed); + let df = cur_ft.wrapping_sub(prev_fasttick); + prev_fasttick = cur_ft; + (df as f64 / dt).round() as f32 + } else { + 0.0 + }; prev_tick = now; // The guest has shut down when the CPU thread has stopped // (soft power-off calls Machine::stop) or has retired no @@ -345,7 +361,7 @@ fn worker_loop( let pcap_status = machine.as_ref() .map_or(iris::net::PcapStatus::Inactive, |m| m.net_pcap_status()); let _ = evt_tx.send(Evt::Status(Status { - mips, cpu_halted, cpu_stopped, chd_sync_pending, + mips, kernel_hz, cpu_halted, cpu_stopped, chd_sync_pending, net_frames, net_guest_ip, net_guest_gateway, net_nat_gateway, pcap_status, ..Status::default() @@ -408,6 +424,9 @@ fn worker_loop( // Latch REX3's cycle counter for the live MIPS estimate. cycles = m.get_display().map(|d| d.cycles()); prev_cycles = cycles.map(|c| c.get()).unwrap_or(0); + let ft = m.fasttick_count(); + prev_fasttick = ft.load(std::sync::atomic::Ordering::Relaxed); + fasttick = Some(ft); prev_tick = std::time::Instant::now(); machine = Some(m); let _ = evt_tx.send(Evt::Started); @@ -443,6 +462,7 @@ fn worker_loop( if let Some(m) = machine.take() { *ps2_slot.lock() = None; cycles = None; + fasttick = None; // Always report the machine as stopped so the user regains // control, even if the stop failed or had to be abandoned. if let Err(msg) = stop_machine_timed(m) { @@ -462,6 +482,7 @@ fn worker_loop( if let Some(mut m) = machine.take() { *ps2_slot.lock() = None; cycles = None; + fasttick = None; m.stop(); synced = match m.sync_chd_disks( None, diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index 32bde775..b54e4512 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -382,7 +382,7 @@ struct MissingDisk { /// grant has lapsed — so opening alone would still slip past the check and only /// fail deep in the CHD/image loader. Catching it here routes the disk to the /// missing-disk modal, where the user can re-select or detach it. -fn disk_readable(path: &str) -> bool { +pub(crate) fn disk_readable(path: &str) -> bool { use std::io::Read; if path.is_empty() { return false; @@ -1332,6 +1332,7 @@ impl App { } else { ui.label(RichText::new("Applied at next Start").weak().small()); } + let ip28_banks = self.cfg.machine.profile.ip28(); ui.separator(); ui.label("Quick presets (auto-distributed):"); for &p in RAM_PRESETS { @@ -1340,7 +1341,7 @@ impl App { .on_disabled_hover_text("Stop the VM to change RAM") .clicked() { - self.cfg.banks = distribute_ram(p); + self.cfg.banks = distribute_ram(p, ip28_banks); self.mark_dirty(); self.toast(format!("RAM set to {} ({:?})", ram_summary(&self.cfg.banks), self.cfg.banks)); ui.close(); @@ -1352,8 +1353,12 @@ impl App { ui.menu_button(format!("Bank {i}: {} MB", self.cfg.banks[i]), |ui| { for &sz in iris::config::VALID_BANK_SIZES { if ui - .add_enabled(!running, egui::Button::new(format!("{sz} MB"))) - .on_disabled_hover_text("Stop the VM to change RAM") + .add_enabled(!running && (sz != 256 || ip28_banks), egui::Button::new(format!("{sz} MB"))) + .on_disabled_hover_text(if running { + "Stop the VM to change RAM" + } else { + "256 MB banks need the IP28 machine profile" + }) .clicked() { self.cfg.banks[i] = sz; @@ -1799,6 +1804,12 @@ impl App { "MIPS = instructions per wall-clock second on your PC (real emulation speed).\n\ IRIX System Manager \"MHz\" from hinv is inventory from the PROM — not host performance.", ); + ui.label(format!("{:.0} Hz", self.emu.status.kernel_hz)) + .on_hover_text( + "The kernel's own clock-tick rate — CP0 Compare matches, or the 8254 \ + timer interrupts IRIX uses instead when it can (they never both run). \ + Distinct from MIPS: this is what IRIX keeps time with.", + ); } // Networking indicator — ONE badge that shows both liveness (the dot's // colour) and the active backend (the label: NAT / PCAP). Grey while diff --git a/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md b/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md new file mode 100644 index 00000000..529cf918 --- /dev/null +++ b/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md @@ -0,0 +1,27 @@ +# A new graphics board needs no `iris-gui` capture wiring + +`iris-gui` never special-cases which graphics device is active. `handle.rs` +installs the capture renderer via `Machine::get_display()` / +`get_display_head1()`, which return `Option>` +(`src/machine.rs`) — a single trait object, picked in this order: GR2, then +IMPACT (mgras), then REX3. Adding IMPACT (commit `0b1ea38`) required zero +changes to `framebuffer.rs` or `handle.rs`; it only needed `Machine::get_display` +in core to know about `_phys.mgras`, which it already did by the time the GUI +work started. + +So when a new board lands in core and implements `GfxDisplay` +(`src/gfx_display.rs`), the GUI's framebuffer path picks it up automatically. +What *does* need GUI-side work for a new board: + +- A way to select it that doesn't collide with the existing board field(s). + IMPACT lives in a separate `[impact]` config section from `[graphics].board` + (they share the GIO gfx slot and `validate()` refuses both at once), so + `iris-gui/src/config_ui.rs` unifies them into one `GfxChoice` picker rather + than adding a second dropdown — see `show_board_picker`. +- Hiding controls that don't apply (Newport heads, VC2 resolution presets) + when the new board is active. +- Any config field the new board doesn't share with existing ones (bank + sizes, PROM requirements, etc.). + +Don't assume a black window on a new board means the capture path needs +touching — check `Machine::get_display()` first. diff --git a/src/lib.rs b/src/lib.rs index 91d98e8b..e010b3b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,19 @@ 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 Indigo2 IMPACT (IP28) machine profile and its R10000 CPU model. + /// Implies `ppmem`. The GUI gates the IP28/R10000 entries in the machine + /// profile and CPU dropdowns on this. + pub const IP28: bool = cfg!(feature = "ip28"); + /// Host services for IRIX programs (private syscalls 3000-3009, + /// `iris-hostcall`). No per-machine config — a guest either gets the trap + /// or doesn't, decided at build time. + pub const HOSTCALL: bool = cfg!(feature = "hostcall"); + /// Host OpenGL for IRIX programs over host call 3000 (`iris-hostgl`). + /// Implies `hostcall`. Only macOS has a backend (CGL) — `hostgl` can be + /// built elsewhere, but `register()` then has nothing to register, so + /// this is `false` off macOS even in a `--features hostgl` build. + pub const HOSTGL: bool = cfg!(feature = "hostgl") && cfg!(target_os = "macos"); // 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 @@ -130,6 +143,9 @@ pub mod build_features { ("rex-jit", cfg!(feature = "rex-jit")), ("lightning", cfg!(feature = "lightning")), ("ppmem", cfg!(feature = "ppmem")), + ("ip28", cfg!(feature = "ip28")), + ("hostcall", cfg!(feature = "hostcall")), + ("hostgl", cfg!(feature = "hostgl")), ("tcache", cfg!(feature = "tcache")), ("tlbvmap", cfg!(feature = "tlbvmap")), ("tlbstats", cfg!(feature = "tlbstats")), diff --git a/src/machine.rs b/src/machine.rs index 167d3f2a..d060b35a 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1337,6 +1337,15 @@ impl Machine { self.cpu.is_running() } + /// The kernel's clock-tick counter — CP0 Compare matches, or the IOC's + /// 8254 timer 0/1 interrupts when IRIX uses those instead (see + /// `Ioc::set_clock_ticks`; the two never both run). Read a delta over wall + /// time to get the guest's Hz, the same math the CLI status bar's + /// `BarStats::fasttick` uses (`src/disp.rs`). + pub fn fasttick_count(&self) -> Arc { + self.cpu.fasttick_count() + } + /// Number of attached CHD disks whose `.diff.chd` holds changes pending a /// fold-back into the base on a clean shutdown (the "Synchronizing disks" /// step). 0 means a clean exit needs no disk sync.