From 8b4aaa0005590b47a09152e70d65994c13ad945e Mon Sep 17 00:00:00 2001 From: Dani Sarfati Date: Thu, 2 Jul 2026 20:56:30 -0400 Subject: [PATCH] iris-gui: drop baked status bar; add model/resolution to New Machine Two GUI tweaks: - Stop baking the emulator status bar (MIPS/NET/SCSI/LED) into the guest frame. The GUI capture renderers drew it at height-16, which both duplicated iris-gui's own egui status chrome and overwrote the guest's bottom 16 scanlines (the native iris window still draws it as a separate strip below the display, unaffected). The status_bar_only heartbeat tick now early-returns since there's nothing bar-specific left to push. - Add "Machine model" (MachineProfile) and "Display resolution" (NewportResolution) dropdowns to the New Machine dialog, reusing the existing enums. Added a `pub const ALL` to each and pointed both the Config tab and the new dialog at it, so the variant lists live in one place and can't drift. Co-Authored-By: Claude Opus 4.8 (1M context) --- iris-gui/src/config_ui.rs | 12 ++-- iris-gui/src/dialogs/new_machine.rs | 34 +++++++++- iris-gui/src/framebuffer.rs | 100 ++++++---------------------- src/config.rs | 4 ++ src/vc2_timings.rs | 9 +++ 5 files changed, 68 insertions(+), 91 deletions(-) diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 313a9e1..c17dfce 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -269,8 +269,9 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { ComboBox::from_id_salt("machine_profile") .selected_text(cfg.machine.profile.label()) .show_ui(ui, |ui| { - ui.selectable_value(&mut cfg.machine.profile, MachineProfile::IndyIp24, MachineProfile::IndyIp24.label()); - ui.selectable_value(&mut cfg.machine.profile, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip22.label()); + for p in MachineProfile::ALL { + ui.selectable_value(&mut cfg.machine.profile, p, p.label()); + } }); ui.label( RichText::new( @@ -383,12 +384,7 @@ fn show_resolution_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { ComboBox::from_id_salt("graphics_resolution") .selected_text(cfg.graphics.resolution.label()) .show_ui(ui, |ui| { - for mode in [ - NewportResolution::Guest, - NewportResolution::Res1024x768, - NewportResolution::Res1280x960, - NewportResolution::Res1280x1024, - ] { + for mode in NewportResolution::ALL { ui.selectable_value(&mut cfg.graphics.resolution, mode, mode.label()); } }); diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index 4854ca9..a5830b9 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -1,5 +1,6 @@ use eframe::egui::{self, Color32, ComboBox, Grid, RichText, TextEdit}; -use iris::config::{MachineConfig, ScsiDeviceConfig, VALID_BANK_SIZES}; +use iris::config::{MachineConfig, MachineProfile, ScsiDeviceConfig, VALID_BANK_SIZES}; +use iris::vc2_timings::NewportResolution; use crate::ram::RAM_PRESETS; @@ -8,6 +9,10 @@ use crate::ram::RAM_PRESETS; pub struct NewMachineDialog { open: bool, pub name: String, + /// Emulated SGI machine model (Indy IP24 / Indigo2 IP22). + pub profile: MachineProfile, + /// Host-forced Newport video mode (`Guest` leaves it to IRIX/setmon). + pub resolution: NewportResolution, pub prom_path: String, pub use_embedded_prom: bool, pub nvram_path: String, @@ -32,6 +37,8 @@ impl Default for NewMachineDialog { Self { open: false, name: "indy".into(), + profile: MachineProfile::default(), + resolution: NewportResolution::default(), prom_path: "prom.bin".into(), use_embedded_prom: true, nvram_path: crate::settings::GuiSettings::default_nvram_path(), @@ -74,13 +81,34 @@ impl NewMachineDialog { .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { ui.set_min_width(440.0); - ui.label(RichText::new("Configure a new SGI Indy emulation").strong()); + ui.label(RichText::new("Configure a new SGI machine").strong()); ui.add_space(4.0); Grid::new("new_machine_grid").num_columns(2).striped(true).show(ui, |ui| { ui.label("Name"); ui.add(TextEdit::singleline(&mut self.name).desired_width(200.0)); ui.end_row(); + + ui.label("Machine model"); + ComboBox::from_id_salt("nm_profile") + .selected_text(self.profile.label()) + .show_ui(ui, |ui| { + for p in MachineProfile::ALL { + ui.selectable_value(&mut self.profile, p, p.label()); + } + }); + ui.end_row(); + + ui.label("Display resolution"); + ComboBox::from_id_salt("nm_resolution") + .selected_text(self.resolution.label()) + .show_ui(ui, |ui| { + for mode in NewportResolution::ALL { + ui.selectable_value(&mut self.resolution, mode, mode.label()); + } + }); + ui.end_row(); + ui.label("PROM image"); ui.horizontal(|ui| { ui.add_enabled(!self.use_embedded_prom, @@ -197,6 +225,8 @@ impl NewMachineDialog { .fill(Color32::from_rgb(60, 110, 60))).clicked() { let mut cfg = MachineConfig::default(); + cfg.machine.profile = self.profile; + 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 // (the load path warns + falls back when the file is missing). diff --git a/iris-gui/src/framebuffer.rs b/iris-gui/src/framebuffer.rs index 61db910..02e0b8b 100644 --- a/iris-gui/src/framebuffer.rs +++ b/iris-gui/src/framebuffer.rs @@ -6,7 +6,7 @@ use iris::compositor::{Compositor, SwCompositor}; use iris::debug_overlay::DebugOverlay; -use iris::disp::{BarStats, Rex3Screen, StatusBar, StatusBarTexture, STATUS_BAR_HEIGHT}; +use iris::disp::{BarStats, Rex3Screen, StatusBar, StatusBarTexture}; use iris::gl_compositor::GlCompositor; use iris::headless_gl::HeadlessGl; use iris::rex3::Renderer; @@ -71,19 +71,14 @@ impl CaptureRenderer { } } -/// IRIX status strip is the bottom `STATUS_BAR_HEIGHT` rows of the visible frame. -fn status_bar_y(height: usize) -> usize { - height.saturating_sub(STATUS_BAR_HEIGHT) -} - impl Renderer for CaptureRenderer { fn present( &mut self, screen: &mut Rex3Screen, _overlay: &mut DebugOverlay, - status: &mut StatusBar, + _status: &mut StatusBar, _sbtex: &mut StatusBarTexture, - stats: &BarStats, + _stats: &BarStats, _need_readback: bool, live_fb_rgb: Option<&[u32]>, live_fb_aux: Option<&[u32]>, @@ -93,35 +88,24 @@ impl Renderer for CaptureRenderer { let height = screen.height; if width == 0 || height == 0 { return; } + // Heartbeat-only refresh: the compositor sets `status_bar_only` when + // nothing in the guest display changed and only the emulator's own + // status bar would. iris-gui shows its own status chrome and never bakes + // that bar into the frame, so there's nothing to push here. + if screen.status_bar_only && self.sink.seq() > 0 { + return; + } + let needed = 2048 * 1024; if self.last_pixels.len() < needed { self.last_pixels.resize(needed, 0); } - if screen.status_bar_only && self.sink.seq() > 0 { - status.update(stats.hb); - let bar_y = status_bar_y(height); - status.render(&mut self.last_pixels, width, bar_y, stats); - pack_rows( - &self.sink, - &self.last_pixels, - width, - height, - bar_y, - STATUS_BAR_HEIGHT, - &mut self.seq, - ); - return; - } - let src = screen.compositor_source_from(live_fb_rgb, live_fb_aux); self.compositor.compose_pixels(&src); drop(src); self.last_pixels.copy_from_slice(self.compositor.pixels()); - status.update(stats.hb); - status.render(&mut self.last_pixels, width, status_bar_y(height), stats); - pack_sw_frame(&self.sink, &self.last_pixels, width, height, &mut self.seq); } @@ -158,9 +142,9 @@ impl Renderer for GlCaptureRenderer { &mut self, screen: &mut Rex3Screen, _overlay: &mut DebugOverlay, - status: &mut StatusBar, + _status: &mut StatusBar, _sbtex: &mut StatusBarTexture, - stats: &BarStats, + _stats: &BarStats, _need_readback: bool, live_fb_rgb: Option<&[u32]>, live_fb_aux: Option<&[u32]>, @@ -170,27 +154,16 @@ impl Renderer for GlCaptureRenderer { let height = screen.height; if width == 0 || height == 0 { return; } - let needed = 2048 * 1024; - if self.last_pixels.len() < needed { - self.last_pixels.resize(needed, 0); - } - + // See CaptureRenderer::present — no baked status bar, so a heartbeat-only + // refresh has nothing to push. if screen.status_bar_only && self.sink.seq() > 0 { - status.update(stats.hb); - let bar_y = status_bar_y(height); - status.render(&mut self.last_pixels, width, bar_y, stats); - pack_rows( - &self.sink, - &self.last_pixels, - width, - height, - bar_y, - STATUS_BAR_HEIGHT, - &mut self.seq, - ); return; } + let needed = 2048 * 1024; + if self.last_pixels.len() < needed { + self.last_pixels.resize(needed, 0); + } if self.rgba.len() < needed { self.rgba.resize(needed, 0); } @@ -201,9 +174,6 @@ impl Renderer for GlCaptureRenderer { self.compositor.readback_to_screen(&mut self.rgba, width, height, gl); self.last_pixels.copy_from_slice(&self.rgba[..needed]); - status.update(stats.hb); - status.render(&mut self.last_pixels, width, status_bar_y(height), stats); - pack_sw_frame(&self.sink, &self.last_pixels, width, height, &mut self.seq); } @@ -241,38 +211,6 @@ fn pack_sw_frame(sink: &FrameSink, pixels: &[u32], width: usize, height: usize, sink.seq.store(*seq, Ordering::Release); } -fn pack_rows( - sink: &FrameSink, - pixels: &[u32], - width: usize, - height: usize, - start_y: usize, - row_count: usize, - seq: &mut u64, -) { - let mut frame = sink.lock(); - if frame.width != width || frame.height != height || frame.rgba.is_empty() { - pack_sw_frame(sink, pixels, width, height, seq); - return; - } - - frame.dirty_y = start_y as u32; - frame.dirty_h = row_count as u32; - - for y in start_y..start_y + row_count.min(height.saturating_sub(start_y)) { - let src_row = &pixels[y * 2048..y * 2048 + width]; - let dst_row = &mut frame.rgba[y * width * 4..(y + 1) * width * 4]; - for (dst_px, &word) in dst_row.chunks_exact_mut(4).zip(src_row) { - dst_px.copy_from_slice(&(word | 0xFF00_0000).to_le_bytes()); - } - } - - *seq = seq.wrapping_add(1); - frame.seq = *seq; - drop(frame); - sink.seq.store(*seq, Ordering::Release); -} - /// Pick CPU or GL capture renderer based on `IRIS_GUI_GL=1`. pub fn new_capture_renderer(sink: FrameSink) -> Box { if std::env::var("IRIS_GUI_GL").map(|v| v == "1").unwrap_or(false) { diff --git a/src/config.rs b/src/config.rs index 0e9897d..248d41c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -217,6 +217,10 @@ pub enum MachineProfile { } impl MachineProfile { + /// All selectable profiles, in display order. Single source of truth for the + /// GUI dropdowns (Config tab + New Machine dialog) so they never drift. + pub const ALL: [Self; 2] = [Self::IndyIp24, Self::Indigo2Ip22]; + pub fn label(self) -> &'static str { match self { Self::IndyIp24 => "SGI Indy (IP24)", diff --git a/src/vc2_timings.rs b/src/vc2_timings.rs index 1190fe4..1b436aa 100644 --- a/src/vc2_timings.rs +++ b/src/vc2_timings.rs @@ -30,6 +30,15 @@ pub enum NewportResolution { } impl NewportResolution { + /// All selectable modes, in display order. Single source of truth for the + /// GUI dropdowns (Config tab + New Machine dialog) so they never drift. + pub const ALL: [Self; 4] = [ + Self::Guest, + Self::Res1024x768, + Self::Res1280x960, + Self::Res1280x1024, + ]; + pub fn label(self) -> &'static str { match self { Self::Guest => "Guest (IRIX / setmon)",