diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f62de9..1a116925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,14 @@ hashes are given where a change is easiest to understand by reading the commit. load. The monitor's `scsi` and `cow` commands (`scsi1`/`cow1` for the external bus) work on the O2's Adaptec controllers: status, disc changes, overlay commit and reset, and the chip's registers. +- **One configuration.** The O2 is set up with the shared keys: `prom`, + `nvram` (the PROM's flash), `banks` (the kit in each DIMM pair), + `serial_port_a`/`serial_port_b`/`serial_log`, `headless`, `[clock] + fixed_mhz` (CP0 Count, half the CPU clock), `[network]` with `mac` and the + new `mode = "off"`, `[[port_forward]]`, `nat_subnet` and `[nfs]`. `[o2]` + keeps `mouse`, `control_port` and `arcs_fpu`; its old keys are folded into + the shared ones on load, with a note. The second serial port is served too, + and the flash is kept between runs by default. - **Scripting and inspection.** The O2's devices have monitor commands, and `o2 ` (also on `[o2] control_port`) takes screen shots, types, moves the mouse and reads memory ([debug.md](debug.md#sgi-o2-ip32)). @@ -47,6 +55,12 @@ hashes are given where a change is easiest to understand by reading the commit. page in six. IRIX on an R5000 O2 panicked with "init died" for the same reason; it now boots to the desktop. +### Memory configuration + +- **`banks` has eight entries.** Every machine today uses the first four and + the rest must be 0; a config may list fewer, and files are written back with + four. Room for machines with more banks. + ### SCSI configuration - **One id on both controllers.** A `[scsi.N]` key is a slot: 1–7 on diff --git a/FEATURES.md b/FEATURES.md index 30faca04..e7cc5f4b 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -138,17 +138,17 @@ the first `[section]`, because TOML tables continue until the next header. | Key | Default | Values and behavior | |---|---|---| -| `prom` | `"prom.bin"` | PROM path; IP24, IP22, and IP28 each have an embedded fallback. Indigo2 also tries `070-1367-012.bin` (IP22) or `070-1477-002.bin` (IP28) in the working directory. | -| `nvram` | `"nvram.bin"` | DS1386 backing file; GUI defaults to an absolute user-config path. | +| `prom` | `"prom.bin"` | PROM path; IP24, IP22, and IP28 each have an embedded fallback. Indigo2 also tries `070-1367-012.bin` (IP22) or `070-1477-002.bin` (IP28) in the working directory; the O2, which has no embedded PROM, tries `ip32prom.rev4.18.bin`. | +| `nvram` | `"nvram.bin"` | DS1386 backing file on the Indy; the PROM's flash (its environment) on the O2. GUI defaults to an absolute user-config path. | | `nveeprom` | `"nveeprom.bin"` | Motherboard EEPROM file; GUI anchors it in its user-config directory. | -| `banks` | `[128,128,0,0]` | Four RAM bank sizes in MB; see profile constraints below. | +| `banks` | `[128,128,0,0]` | RAM bank sizes in MB, up to eight entries (every machine today has four; the rest must be 0). On the O2 each entry is a DIMM pair's kit (32, 64, 128 or 256), largest first. See profile constraints below. | | `scale` | `1` | Standalone window scale: 1–4. GUI uses its separate vm_scale. | -| `headless` | `false` | Disable the standalone window and Newport graphics; audio is independent. | +| `headless` | `false` | Disable the standalone window and graphics; the O2 then boots on its serial console. Audio is independent. | | `no_audio` | `false` | Disable HAL2 audio emulation. | | `gdb_port` | `unset` | Start the GDB TCP stub; debugging is unavailable in lightning builds. | | `monitor_port` | `8888 when unset` | Monitor listener on loopback. | -| `serial_port_a` | `8880 when unset` | SCC channel A listener (tty2). | -| `serial_port_b` | `8881 when unset` | SCC channel B listener (tty1/console); GUI halt uses this port. | +| `serial_port_a` | `8880 when unset` | SCC channel A listener (tty2); the O2's first serial port, its console when headless. | +| `serial_port_b` | `8881 when unset` | SCC channel B listener (tty1/console); the O2's second serial port. GUI halt uses this port. | | `load_elf` | `unset` | Load a static big-endian MIPS ELF32 binary before starting the CPU. | | `test_device` | `false` | Map the bare-metal test device in GIO slot 0. | | `test_device_dump` | `iris-testdev-dump.json when unset` | Test-device machine-state JSON output. | @@ -159,7 +159,7 @@ the first `[section]`, because TOML tables continue until the next header. | `ci_display` | `false` | Keep host graphics window visible in CI mode. | | `mouse_scroll_pixels_per_line` | `40.0` | Host scrolling distance per guest PS/2 detent. | | `lock_aspect_ratio` | `true` | Constrain standalone window aspect ratio; false allows letterboxing. | -| `serial_log` | `unset` | Append guest channel-B output; used by CI and GUI serial capture. | +| `serial_log` | `unset` | Append guest channel-B output (the O2's first serial port); used by CI and GUI serial capture. | | `scsi_deferred_int` | `true` | Defer SCSI status interrupts for BSD compatibility. | ### [machine] @@ -204,7 +204,7 @@ An explicit non-disk kind takes precedence over the legacy cdrom flag. | Key | Default | Values and behavior | |---|---|---| -| `network.mode` | `nat` | nat or pcap; PCAP needs build support and host capture permissions. | +| `network.mode` | `nat` | nat, pcap or off (no Ethernet; O2 only for now). PCAP needs build support and host capture permissions, and is not available on the O2 yet. | | `network.pcap_interface` | `auto-pick when unset` | Interface name or numeric list index; PCAP only. | | `network.nfs_pcap_ip` | `unset` | Virtual LAN IP for the built-in NFS responder in PCAP mode. | | `network.mac` | `08:00:69:12:34:56 when unset` | ec0 MAC; injected only into blank PROM battery-backed MAC slots. | @@ -274,7 +274,7 @@ Omit `[nfs]` to disable the share; the default forward list is empty. | Key | Default | Values and behavior | |---|---|---| -| `clock.fixed_mhz` | `33 on IP22/IP24; 97.5 on IP28 when unset` | CP0 Count MHz; IRIX CPU inventory reports twice this rate. | +| `clock.fixed_mhz` | `33 on IP22/IP24; 97.5 on IP28; 90 on the O2 when unset` | CP0 Count MHz; IRIX's CPU inventory and the O2's PROM report twice this rate. | ### [rtc_offset] @@ -295,32 +295,33 @@ Omit `[nfs]` to disable the share; the default forward list is empty. ### [o2] -Used with `machine.profile = "o2_ip32"`. The O2's disks are the `[scsi.N]` -tables above, with `controller` as the bus (0 internal, 1 external); the -Indy/Indigo2 top-level `prom`, `nvram` and `banks` keys do not apply. Paths are -relative to where iris runs. See [the O2 guide](docs/o2-ip32.md). An older -config's `o2.disks` list is folded into `[scsi.N]` when the file is loaded and -reported as deprecated. +Used with `machine.profile = "o2_ip32"`. An O2 is configured by the shared +keys: `prom` (its PROM image, not embedded; `ip32prom.rev4.18.bin` is tried +when `prom` is left at its default), `nvram` (the PROM's flash, which holds +its environment), `banks` (the kit in each of the four DIMM pairs: 32, 64, +128 or 256 MB, largest first), `serial_port_a`/`serial_port_b`/`serial_log`, +`headless` (serial console instead of the graphics console and keyboard), +`[clock] fixed_mhz` (CP0 Count; the CPU runs at twice it, so 90 MHz is a +180 MHz O2), `[network]` including `mac` and `mode = "off"` for an unplugged +Ethernet, `[[port_forward]]`, `nat_subnet`, `[nfs]` and `[scsi.N]` +(`controller` is the bus). See [the O2 guide](docs/o2-ip32.md). The old +O2-only spellings (`o2.prom`, `o2.flash`, `o2.ram_mb`, `o2.dimms`, +`o2.disks`, `o2.serial_port`, `o2.serial_log`, `o2.graphics`, `o2.cpu_mhz`, +`o2.net`, `o2.forwards`) are deprecated: they are folded into those keys when +the file is loaded, with a note, and never written back. | Key | Default | Values and behavior | |---|---|---| -| `o2.prom` | `ip32prom.rev4.18.bin` | The O2 PROM image (not embedded). | -| `o2.flash` | unset | File the PROM's flash (its environment) is loaded from and saved to. | -| `o2.ram_mb` | `256` | Memory in MB, fitted largest DIMM kits first (up to 1024). | -| `o2.dimms` | unset | Instead of `ram_mb`: the kit in each DIMM pair, in MB (32, 64, 128 or 256). | -| `o2.serial_port` | unset | TCP port on 127.0.0.1 for the first serial port. | -| `o2.serial_log` | unset | File that copies the first serial port's output. | -| `o2.graphics` | `true` | PS/2 keyboard and the graphics console; `false` for a serial console. | -| `o2.mouse` | `true` | PS/2 mouse on the second port (with `graphics`). | -| `o2.cpu_mhz` | `180` | CPU clock the PROM measures; CP0 Count runs at half of it. | +| `o2.mouse` | `true` | PS/2 mouse on the second port (with the graphics console). | | `o2.control_port` | unset | TCP port on 127.0.0.1 serving the `o2` monitor commands for scripts. | -| `o2.net` | `true` | Ethernet on the NAT gateway. | -| `o2.forwards` | `[]` | Host-to-guest port forwards: `{ proto, host_port, guest_port }`. | | `o2.arcs_fpu` | `false` | Test aid: lend a kernel the FPU while it runs inside the PROM. | ### Hardware constraints and precedence -Bank values are 0, 8, 16, 32, 64, or 128 MB; only IP28 accepts 256/512 MB. +Bank values are 0, 8, 16, 32, 64, or 128 MB; only IP28 accepts 256/512 MB. On +the O2 a bank is a DIMM pair's kit, 32, 64, 128 or 256 MB, fitted from pair A +largest first. `banks` has eight entries; every machine today uses the first +four and the rest must be 0. IP22/IP24 four-bank IRIX 6.5 behavior remains separate from IP28's verified two-bank 1 GB layout. Extreme requires IP22. All non-Newport boards require `heads=1` and `resolution="guest"`. IP28 IRIX needs IMPACT; its PROM has an diff --git a/HELP.md b/HELP.md index 8a73ec79..dd1becce 100644 --- a/HELP.md +++ b/HELP.md @@ -62,6 +62,10 @@ On IP22/IP24, each bank is at most 128 MB. IP28 also supports 256 MB and [IP28 512 MB banks](clanker/devices/ip28-512mb-banks.md). The table below describes the IP22/IP24 extended layouts. +`banks` has room for eight entries; every machine today uses four, and the +rest must be 0. On the O2 each entry is the kit in a DIMM pair (32, 64, 128 or +256 MB), largest first: `[256, 256, 256, 256]` is 1 GB. + `banks` in `iris.toml` / iris-gui is applied only when you **Start** the VM. Changing RAM while IRIX is running updates the config but not the live guest — **Stop → change banks → Start**. @@ -500,15 +504,16 @@ ignored. The checked-in `iris.toml` is a commented example of everything in this ```toml # ── Top-level scalars (must come before any [section]) ─────────────────────── -prom = "prom.bin" # PROM image; the embedded one is used if missing -nvram = "nvram.bin" # DS1386 NVRAM/RTC file (Indy). Use one per install. +prom = "prom.bin" # PROM image; the embedded one is used if missing (the O2 has none: its 4.18 image) +nvram = "nvram.bin" # DS1386 NVRAM/RTC file (Indy); the PROM's flash (O2). Use one per install. nveeprom = "nveeprom.bin" # 93CS56 NVRAM EEPROM file (Indigo2 only) # RAM bank sizes in MB: 0 (absent), 8, 16, 32, 64, 128; IP28 also 256/512. +# O2: the kit in each DIMM pair (32/64/128/256), largest first. Up to eight entries. banks = [128, 128, 0, 0] scale = 1 # window scale, kept across guest mode changes; --2x overrides -headless = false # no window, no REX3 (audio unaffected) +headless = false # no window, no graphics; the O2 boots on its serial console (audio unaffected) no_audio = false # no HAL2 / cpal (graphics unaffected) lock_aspect_ratio = true # false = free resize with letterboxing mouse_scroll_pixels_per_line = 40 diff --git a/clanker/devices/ip32-o2-bringup-log.md b/clanker/devices/ip32-o2-bringup-log.md index 07e072e7..30ec71a8 100644 --- a/clanker/devices/ip32-o2-bringup-log.md +++ b/clanker/devices/ip32-o2-bringup-log.md @@ -2,16 +2,18 @@ Status: history, 2026-09 to 2026-10. How the O2 was brought up, gate by gate, with the dead ends. For the machine as it is, read [docs/o2-ip32.md](../../docs/o2-ip32.md); -the "Today" section below summarises it as of 2026-10-23, and the rest is the +the "Today" section below summarises it as of 2026-10-24, and the rest is the journal, in order. ## Today (read this first) -The O2 runs as a machine like the others: `profile = "o2_ip32"` with an -`[o2]` section (PROM, flash, `ram_mb` or `dimms`, `disks` on controllers 0 -and 1, `serial_port`, `graphics`, `mouse`, `net` and `forwards`, -`control_port`, `cpu_mhz`), started with `iris --config`. Its -devices are on the shared monitor (`monitor_port`, default 8888). +The O2 runs as a machine like the others: `profile = "o2_ip32"`, configured +by the shared keys (`prom`, `nvram` for the PROM's flash, `banks` as DIMM +kits, `[scsi.N]` on controllers 0 and 1, `serial_port_a`/`b`, `headless`, +`[network]`, `[clock] fixed_mhz`) with `[o2]` holding only `mouse`, +`control_port` and `arcs_fpu`; the old `[o2]` spellings of the rest are +folded in on load (2026-10-24). Started with `iris --config`. Its devices +are on the shared monitor (`monitor_port`, default 8888). How it is put together now: diff --git a/docs/o2-ip32.md b/docs/o2-ip32.md index ddf8bacf..bfff1e1f 100644 --- a/docs/o2-ip32.md +++ b/docs/o2-ip32.md @@ -15,23 +15,27 @@ The O2 does not run in iris-gui yet; start it from the `iris` command line. ## Configure the machine -The O2 has its own `[o2]` section for what only it has. Its disks are the -same `[scsi.N]` tables as the other machines'; the Indy and Indigo2 top-level -keys `prom`, `nvram` and `banks` do not apply to it. +An O2 is set up with the same keys as the other machines; `[o2]` holds only +what the O2 alone has (`mouse`, `control_port`, `arcs_fpu`). ```toml +prom = "ip32prom.rev4.18.bin" # the O2 PROM (not built in); tried when prom is left unset +nvram = "flash.bin" # the PROM's flash, where setenv settings live +banks = [256, 256, 256, 256] # the kit in each DIMM pair, largest first: 1 GB +serial_port_a = 2291 # first serial port (the console when headless) +serial_log = "serial.log" + [machine] profile = "o2_ip32" cpu = "r10000" # or "r5000" [o2] -prom = "ip32prom.rev4.18.bin" -flash = "flash.bin" -ram_mb = 1024 -serial_port = 2291 -serial_log = "serial.log" control_port = 2293 -forwards = [ { proto = "tcp", host_port = 2292, guest_port = 22 } ] + +[[port_forward]] +proto = "tcp" +host_port = 2292 +guest_port = 22 [scsi.1] path = "irix6522.raw" @@ -42,13 +46,21 @@ cdrom = true ``` Paths are relative to the directory iris runs in. Every key is listed in -[FEATURES.md](../FEATURES.md#o2). +[FEATURES.md](../FEATURES.md), the O2's own under [`[o2]`](../FEATURES.md#o2). The PROM image is not built in: supply the O2 PROM (4.18 is the last -release) as `prom`. The O2 has no NVRAM chip; the PROM keeps its environment -(`setenv`) in flash. With `flash = "FILE"`, IRIS starts from FILE when it -exists and saves the flash back there whenever the guest writes it, so -settings survive a restart. The PROM image itself is never written. +release) as `prom`; when `prom` is left at its default, iris tries +`ip32prom.rev4.18.bin`. The O2 has no NVRAM chip; the PROM keeps its +environment (`setenv`) in flash, and `nvram` names the file that flash is +kept in between runs (`nvram.bin` unless set): IRIS starts from it when it +exists and saves the flash back whenever the guest writes it, so settings +survive a restart. The PROM image itself is never written. + +Configs from before this layout set all of it in `[o2]` (`prom`, `flash`, +`ram_mb`, `dimms`, `disks`, `serial_port`, `serial_log`, `graphics`, +`cpu_mhz`, `net`, `forwards`). They still load: iris moves each onto the +shared key it stands for, says so once, and never writes them back. Move them +in the file to silence the note. ## CPU, memory and clock @@ -58,10 +70,11 @@ settings survive a restart. The PROM image itself is never written. through the PROM switches back. - **R5000.** The full cache model throughout. Linux supports only R5000-class O2s. -- `cpu_mhz` (default 180) sets the clock the PROM measures and reports; CP0 - Count runs at half of it. R10000 O2s shipped at 175 and 195 MHz. -- `ram_mb` fits memory the usual way, largest DIMM kits first, up to 1024 MB. - `dimms = [128, 128]` gives the kit in each DIMM pair instead. +- `[clock] fixed_mhz` is CP0 Count, 90 MHz unless set; the PROM measures and + reports twice it as the CPU clock. R10000 O2s shipped at 175 and 195 MHz + (87.5 and 97.5), R5000s at 180 to 300. +- `banks` is the kit in each of the four DIMM pairs from A: 32, 64, 128 or + 256 MB each, largest first, up to 1 GB. The default `[128, 128]` is 256 MB. ## Disks and CD-ROMs @@ -100,19 +113,22 @@ will not match it. ## Console, keyboard and mouse -With `graphics = true` (the default) the PROM and IRIX use the graphics -console with a PS/2 keyboard, and `mouse` (default on) adds a PS/2 mouse. -With `graphics = false` the console is serial port 1. +With the graphics console (the default) the PROM and IRIX use a PS/2 +keyboard, and `[o2] mouse` (default on) adds a PS/2 mouse. With +`headless = true` there is no keyboard, and the console is serial port 1. -`serial_port` serves the first serial port on TCP 127.0.0.1 (`telnet -127.0.0.1 PORT`); `serial_log` copies everything it sends to a file. +`serial_port_a` and `serial_port_b` serve the two serial ports on TCP +127.0.0.1 (8880 and 8881 unless set; `telnet 127.0.0.1 PORT`); `serial_log` +copies everything the first one sends to a file. ## Networking -The O2's Ethernet is on IRIS's NAT gateway, as on the other machines: the +The O2's Ethernet is on IRIS's NAT gateway, as on the other machines, and +takes the same settings: `nat_subnet`, `[nfs]`, `[[port_forward]]`, +`[network] mac` (the address in the 1-Wire ID chip) and `tftp_dir`. The guest gets 192.168.0.2 by DHCP, with the gateway and DNS at 192.168.0.1. -`forwards` maps host ports to guest ports (same fields as `[[port_forward]]`). -`net = false` leaves the Ethernet unplugged. +`[network] mode = "off"` leaves the Ethernet unplugged; `pcap` is not +available on the O2 yet. For a PROM network boot, set `[network] tftp_dir = "DIR"` and boot from the PROM's command monitor with `boot -f bootp():FILE`, which loads FILE from DIR. @@ -132,15 +148,14 @@ The monitor's machine commands work as on the other machines: `iris --ci` starts the O2 for [iris-ci](../STORAGE.md) with its CPU stopped and serial port 1 as the CI console: `iris-ci start`, `serial-send`, `serial-wait`, `login`, `run` and `screenshot` work as on the other -machines, and so do `save` and `restore`. `rollback`, the scratch volume and -CD changes do not yet; they answer "not supported on this machine". Boot -with `graphics = false` so the PROM and the operating system use that +machines, and so do `save`, `restore`, the scratch volume and CD changes. +`rollback` does not yet; it answers "not supported on this machine". Boot +with `headless = true` so the PROM and the operating system use that console. ## Known limitations -- No iris-gui support yet. Over iris-ci, no `rollback`, scratch volume or - CD changes. +- No iris-gui support yet. Over iris-ci, no `rollback`. - The PROM cannot load a very large program from disk (it fails on a 16 MB kernel); boot such a file over TFTP instead. - PCI's byte-swapped view of memory is not modelled; every driver tested uses diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 70e12463..9cc76db1 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -16,7 +16,7 @@ use crate::ram::{ram_summary, ram_presets}; #[derive(Clone, Copy, Debug, Default)] pub struct MemoryUiContext { pub running: bool, - pub started_banks: Option<[u32; 4]>, + pub started_banks: Option<[u32; iris::config::MAX_BANKS]>, /// 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, @@ -938,6 +938,8 @@ fn show_network( .selected_text(match cfg.network.mode { NetMode::Nat => t!("config-net-mode-nat"), NetMode::Pcap => t!("config-net-mode-pcap"), + // Only an O2 config can say this, and the GUI does not run one yet. + NetMode::Off => "off".to_string(), }) .show_ui(ui, |ui| { ui.selectable_value(&mut cfg.network.mode, NetMode::Nat, t!("config-net-mode-nat")); diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index 0e934b1a..55711f78 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -63,10 +63,11 @@ impl Default for NewMachineDialog { /// `ip28` gates the 256/512 MB sizes, which only the IP28 MC can express /// (`validate()` rejects them on other machine profiles) — pass /// `cfg.machine.profile.ip28()`. -pub fn distribute_ram(total: u32, ip28: bool) -> [u32; 4] { +pub fn distribute_ram(total: u32, ip28: bool) -> [u32; iris::config::MAX_BANKS] { let mut remaining = total; - let mut banks = [0u32; 4]; - for slot in &mut banks { + let mut banks = [0u32; iris::config::MAX_BANKS]; + // The MC has four banks; the array has room for machines with more. + for slot in banks.iter_mut().take(4) { // Pick the largest size in VALID_BANK_SIZES that is <= remaining. let pick = VALID_BANK_SIZES.iter() .filter(|&&s| s > 0 && s <= remaining && (s <= 128 || ip28)) @@ -322,7 +323,7 @@ impl NewMachineDialog { cfg.nvram = self.nvram_path.clone(); cfg.nveeprom = self.nveeprom_path.clone(); cfg.banks = if self.ram_advanced { - self.ram_banks + iris::config::banks_from4(self.ram_banks) } else { distribute_ram(self.ram_total_mb, self.profile.ip28()) }; @@ -364,8 +365,8 @@ mod ram_tests { #[test] fn ip28_gigabyte_uses_two_banks() { - assert_eq!(distribute_ram(1024, true), [512, 512, 0, 0]); - assert_eq!(distribute_ram(768, true), [512, 256, 0, 0]); - assert_eq!(distribute_ram(512, false), [128, 128, 128, 128]); + assert_eq!(distribute_ram(1024, true), iris::config::banks_from4([512, 512, 0, 0])); + assert_eq!(distribute_ram(768, true), iris::config::banks_from4([512, 256, 0, 0])); + assert_eq!(distribute_ram(512, false), iris::config::banks_from4([128, 128, 128, 128])); } } diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index f7379204..9d1cec82 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -211,7 +211,7 @@ struct App { /// Timestamp of the most recent edit; used to debounce auto-save. cfg_dirty_since: Option, /// Banks passed to the last `Cmd::Start` (guest-visible RAM is fixed until Stop). - started_banks: Option<[u32; 4]>, + started_banks: Option<[u32; iris::config::MAX_BANKS]>, /// The CPU the running guest booted on — see `MemoryUiContext::started_cpu`. started_cpu: Option, tab: Tab, @@ -3796,7 +3796,7 @@ mod machine_storage_integration { std::env::set_var("IRIS_NO_EXIT_ON_POWEROFF", "1"); let mut prefs = GuiSettings::load_in(root.clone(), launch_dir()); let mut cfg = MachineConfig::default(); - cfg.banks = [8, 8, 0, 0]; + cfg.banks = iris::config::banks_from4([8, 8, 0, 0]); cfg.scsi.clear(); cfg.headless = true; cfg.monitor_port = Some(0); @@ -3842,10 +3842,10 @@ mod machine_storage_integration { assert_ne!(settings::nvram_mac(alpha.join("nvram.bin").to_str().unwrap()), settings::nvram_mac(beta.join("nvram.bin").to_str().unwrap())); // Switch rereads external TOML edits rather than the startup map. let mut edited = machines::read(&app.prefs.machine_path("alpha").unwrap()).unwrap(); - edited.banks = [16, 16, 0, 0]; + edited.banks = iris::config::banks_from4([16, 16, 0, 0]); machines::save(&root, "alpha", &mut edited).unwrap(); app.switch_to("alpha"); - assert_eq!(app.cfg.banks, [16, 16, 0, 0]); + assert_eq!(app.cfg.banks, iris::config::banks_from4([16, 16, 0, 0])); app.apply_rename("renamed"); assert_eq!(app.prefs.active_machine.as_deref(), Some("renamed")); assert_eq!(std::env::current_dir().unwrap(), root.join("machines/renamed")); diff --git a/iris-gui/src/ram.rs b/iris-gui/src/ram.rs index 6a72080d..ba0f13b9 100644 --- a/iris-gui/src/ram.rs +++ b/iris-gui/src/ram.rs @@ -9,11 +9,11 @@ pub fn ram_presets(ip28: bool) -> &'static [u32] { if ip28 { IP28_RAM_PRESETS } else { RAM_PRESETS } } -pub fn active_banks(banks: &[u32; 4]) -> usize { +pub fn active_banks(banks: &[u32]) -> usize { banks.iter().filter(|&&s| s > 0).count() } -pub fn ram_summary(banks: &[u32; 4]) -> String { +pub fn ram_summary(banks: &[u32]) -> String { let total: u32 = banks.iter().sum(); let n = active_banks(banks); t!("ram-summary", total = total, banks = n) diff --git a/iris-gui/src/settings.rs b/iris-gui/src/settings.rs index 31b9927d..d400659e 100644 --- a/iris-gui/src/settings.rs +++ b/iris-gui/src/settings.rs @@ -472,11 +472,11 @@ mod storage_tests { // Preference saves do not overwrite externally edited machine TOMLs. let path = prefs.machine_path("indy").unwrap(); let mut cfg = machines::read(&path).unwrap(); - cfg.banks = [8, 8, 0, 0]; + cfg.banks = iris::config::banks_from4([8, 8, 0, 0]); machines::save(&root.0, "indy", &mut cfg).unwrap(); prefs.save().unwrap(); let reloaded = GuiSettings::load_in(root.0.clone(), &root.0); - assert_eq!(reloaded.machines["indy"].banks, [8, 8, 0, 0]); + assert_eq!(reloaded.machines["indy"].banks, iris::config::banks_from4([8, 8, 0, 0])); assert_eq!(std::fs::read_to_string(root.0.join("gui.json.pre-toml.bak")).unwrap(), original_text); } @@ -513,7 +513,7 @@ mod storage_tests { let prefs = GuiSettings::load_in(root.0.clone(), &root.0); assert!(prefs.errors.is_empty(), "{:?}", prefs.errors); assert_eq!(prefs.active_machine.as_deref(), Some("old")); - assert_eq!(prefs.machines["old"].banks, [8, 8, 0, 0]); + assert_eq!(prefs.machines["old"].banks, iris::config::banks_from4([8, 8, 0, 0])); assert!(prefs.last_config.is_none()); assert_eq!(std::fs::read_to_string(source).unwrap(), "banks = [8, 8, 0, 0]"); assert_eq!(GuiSettings::load_in(root.0.clone(), &root.0).machines.len(), 1); diff --git a/iris.toml b/iris.toml index d02fde25..63989616 100644 --- a/iris.toml +++ b/iris.toml @@ -10,7 +10,8 @@ headless = false # Can also be set with --noaudio. no_audio = false -# PROM ROM image (required). +# PROM ROM image. Indy and Indigo2 have an embedded fallback; the O2 needs its +# own (ip32prom.rev4.18.bin is tried when this is left as is). prom = "prom.bin" # Window scale factor. Uses logical points (macOS) / DPI-scaled pixels, so @@ -28,6 +29,8 @@ scale = 1 # Each bank must be 0 (absent), 8, 16, 32, 64, or 128. # The SGI Indy supports up to two populated banks; banks 2 and 3 are # typically absent. Total RAM = sum of non-zero banks. +# Up to eight entries (four used today). On the O2 each entry is the kit in a +# DIMM pair: 32, 64, 128 or 256 MB, largest first. banks = [128, 128, 0, 0] # NAT subnet for the internal network (CIDR notation). diff --git a/src/bench_runner.rs b/src/bench_runner.rs index a41b1ad7..dd6fae00 100644 --- a/src/bench_runner.rs +++ b/src/bench_runner.rs @@ -302,7 +302,7 @@ impl Table { /// graphics, no audio. pub fn bench_config(banks: [u32; 4], cpu: CpuModel) -> MachineConfig { let mut cfg = MachineConfig { - banks, + banks: crate::config::banks_from4(banks), headless: true, no_audio: true, ..Default::default() diff --git a/src/config.rs b/src/config.rs index 2b22205f..a5f27f2c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -323,6 +323,9 @@ pub enum NetMode { /// The guest appears as a real L2 host on the physical LAN; NAT services /// (DHCP/DNS/NFS/port-forward) are NOT provided — use the real network's. Pcap, + /// No Ethernet: the adapter is not plugged into anything. O2 only for now + /// (`MachineConfig::validate`); the Indy and Indigo2 always have a network. + Off, } impl Default for NetMode { @@ -378,6 +381,7 @@ impl Default for NetworkConfig { pub struct NetworkSection { /// Backend: "nat" (default) or "pcap". #[serde(default)] + /// `off` unplugs the Ethernet (O2 only for now). pub mode: NetMode, /// Host interface to bridge onto in PCAP mode (e.g. "eth0", "en0"). /// Run `iris --list-net-interfaces` to enumerate candidates. @@ -511,6 +515,12 @@ impl MachineProfile { } } + /// How many memory banks the machine has: four on all of them (the MC's + /// four banks; the O2's four DIMM pairs). `banks` has room for more. + pub fn memory_banks(self) -> usize { + 4 + } + pub fn guinness(self) -> bool { matches!(self, Self::IndyIp24) } @@ -785,62 +795,70 @@ impl Default for MachineSection { } } -/// An O2 (`[o2]` section). Paths are relative to where iris runs. +/// What only an O2 has (`[o2]` section). The rest of an O2 is the shared +/// keys: `prom` (its PROM image, not embedded), `nvram` (the PROM's flash, +/// which holds its environment), `banks` (the kit in each DIMM pair), +/// `serial_port_a`/`serial_port_b`/`serial_log`, `headless` (a serial +/// console instead of the graphics console), `[clock] fixed_mhz` (CP0 Count, +/// half the CPU clock), `[network]`, `[[port_forward]]` and `[scsi.N]`. The +/// deprecated keys below are the old O2-only spellings of those: +/// [`MachineConfig::fold_deprecated`] moves them over when a file is loaded +/// and they are never written back. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct O2Section { - /// The PROM image (4.18 is the last). - #[serde(default = "default_o2_prom")] - pub prom: String, - /// Where the PROM's flash, and so its environment, is kept between runs. - #[serde(default)] - pub flash: Option, - /// Memory in MB, fitted the usual way (largest kits first). - #[serde(default = "default_o2_ram")] - pub ram_mb: u32, - /// Or the kit in each DIMM pair from A, in MB (32, 64, 128 or 256). - #[serde(default)] - pub dimms: Option>, - /// Deprecated: the O2's SCSI devices are the `[scsi.N]` tables, with - /// `controller` naming the bus (0 internal, 1 external), as on the - /// Indigo2. A `disks` list in an older config is folded into `scsi` when - /// the file is loaded ([`MachineConfig::fold_deprecated`]) and is never - /// written back. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub disks: Vec, - /// A TCP port for the serial console (telnet 127.0.0.1 PORT). - #[serde(default)] - pub serial_port: Option, - /// A file the serial console is copied to. - #[serde(default)] - pub serial_log: Option, - /// A keyboard and the PROM's graphics console. Off: serial console. - #[serde(default = "o2_on")] - pub graphics: bool, - /// The CPU clock in MHz. CP0 Count runs at half of it, and the PROM - /// measures it from Count (it reports it, and passes `cpufreq` to the OS). - /// R10000 O2s shipped at 175 and 195 MHz, R5000s at 180 to 300. - #[serde(default = "default_o2_mhz")] - pub cpu_mhz: u32, - /// A PS/2 mouse on the second port (with `graphics`). + /// A PS/2 mouse on the second port (with the graphics console). #[serde(default = "o2_on")] pub mouse: bool, /// A TCP port on 127.0.0.1 for driving the machine from a script: screen /// shots, keys, the mouse (see `src/ip32_machine.rs`). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub control_port: Option, /// Test aid, not hardware behaviour: lend a kernel the FPU while it is /// inside the PROM (NetBSD prints through the PROM's graphics console /// with CU1 clear, and the PROM scrolls with FPU loads). - #[serde(default)] + #[serde(default, skip_serializing_if = "is_false")] pub arcs_fpu: bool, - /// Ethernet on IRIS's NAT. - #[serde(default = "o2_on")] - pub net: bool, - #[serde(default)] + + /// Deprecated: `prom`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prom: Option, + /// Deprecated: `nvram`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flash: Option, + /// Deprecated: `banks`, fitted largest kits first. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ram_mb: Option, + /// Deprecated: `banks`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dimms: Option>, + /// Deprecated: `[scsi.N]` with `controller` = bus. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub disks: Vec, + /// Deprecated: `serial_port_a`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub serial_port: Option, + /// Deprecated: `serial_log`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub serial_log: Option, + /// Deprecated: `headless` (its inverse). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graphics: Option, + /// Deprecated: `[clock] fixed_mhz`, which is half of it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cpu_mhz: Option, + /// Deprecated: `[network] mode = "off"` when false. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub net: Option, + /// Deprecated: `[[port_forward]]`. + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub forwards: Vec, } +fn is_false(b: &bool) -> bool { + !*b +} + /// An entry of the deprecated `o2.disks` list; see [`O2Section::disks`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -859,18 +877,6 @@ pub struct O2Disk { pub overlay: bool, } -fn default_o2_prom() -> String { - "ip32prom.rev4.18.bin".into() -} - -fn default_o2_mhz() -> u32 { - 180 -} - -fn default_o2_ram() -> u32 { - 256 -} - fn o2_on() -> bool { true } @@ -878,19 +884,19 @@ fn o2_on() -> bool { impl Default for O2Section { fn default() -> Self { Self { - prom: default_o2_prom(), + mouse: true, + control_port: None, + arcs_fpu: false, + prom: None, flash: None, - ram_mb: default_o2_ram(), + ram_mb: None, dimms: None, disks: Vec::new(), serial_port: None, serial_log: None, - graphics: true, - arcs_fpu: false, - mouse: true, - control_port: None, - cpu_mhz: default_o2_mhz(), - net: true, + graphics: None, + cpu_mhz: None, + net: None, forwards: Vec::new(), } } @@ -1012,8 +1018,10 @@ impl Jitv2Config { pub struct ClockConfig { /// CP0 Count frequency in MHz. Count ticks at a fixed rate; there is no /// runtime inference. None selects the profile default: 33 MHz on - /// IP22/IP24 or 97.5 MHz on IP28. IRIX reports twice the Count rate as - /// CPU MHz; host emulation throughput is independent of this setting. + /// IP22/IP24, 97.5 MHz on IP28, 90 MHz on the O2 (a 180 MHz CPU). IRIX + /// reports twice the Count rate as CPU MHz, and the O2's PROM measures + /// it the same way; host emulation throughput is independent of this + /// setting. #[serde(default, skip_serializing_if = "Option::is_none")] pub fixed_mhz: Option, } @@ -1295,9 +1303,15 @@ pub struct MachineConfig { #[serde(default = "default_nveeprom")] pub nveeprom: String, - /// RAM bank sizes in MB. Valid values: 0 (absent), 8, 16, 32, 64, 128. - #[serde(default = "default_banks")] - pub banks: [u32; 4], + /// RAM bank sizes in MB, one per bank, up to [`MAX_BANKS`]. Every machine + /// today has four (`MachineProfile::memory_banks`); the rest must be 0. + /// On the Indy and Indigo2 a bank is 0 (absent), 8, 16, 32, 64 or 128 MB + /// (the IP28 also 256 or 512); on the O2 it is the kit in a DIMM pair, 32, + /// 64, 128 or 256 MB, largest first from pair A. A config may list fewer + /// than eight; the rest are 0, and a config is written back with the + /// trailing zeros beyond the fourth trimmed. + #[serde(default = "default_banks", with = "banks_serde")] + pub banks: [u32; MAX_BANKS], /// Window scale factor (1 = native, 2 = 2× for HiDPI/4K). CLI --2x overrides this. #[serde(default = "default_scale")] @@ -1516,20 +1530,62 @@ pub fn ci_socket_tcp_addr(path: &str) -> String { fn default_scroll_pixels_per_line() -> f64 { 40.0 } fn default_lock_aspect_ratio() -> bool { true } +/// `prom` when a config does not set it. The O2 then tries +/// `ip32_machine::O2_PROM_FILE` as well, having no embedded PROM. +pub const DEFAULT_PROM: &str = "prom.bin"; +/// `nvram` when a config does not set it: the DS1386's file on the Indy, the +/// PROM's flash on the O2. +pub const DEFAULT_NVRAM: &str = "nvram.bin"; + fn default_prom() -> String { - "prom.bin".to_string() + DEFAULT_PROM.to_string() } fn default_nvram() -> String { - "nvram.bin".to_string() + DEFAULT_NVRAM.to_string() } fn default_nveeprom() -> String { "nveeprom.bin".to_string() } -fn default_banks() -> [u32; 4] { - [128, 128, 0, 0] +/// Room in `banks`: eight, for machines with more than today's four. +pub const MAX_BANKS: usize = 8; + +fn default_banks() -> [u32; MAX_BANKS] { + banks_from4([128, 128, 0, 0]) +} + +/// Four bank sizes as a full `banks` array. +pub fn banks_from4(four: [u32; 4]) -> [u32; MAX_BANKS] { + let mut banks = [0u32; MAX_BANKS]; + banks[..4].copy_from_slice(&four); + banks +} + +/// `banks` as TOML: any length from 1 to [`MAX_BANKS`] is read and padded +/// with zeros; it is written with at least four entries and no trailing +/// zeros beyond them, so a four-bank machine's config looks as it always has. +mod banks_serde { + use super::MAX_BANKS; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(banks: &[u32; MAX_BANKS], ser: S) -> Result { + let n = banks.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1).max(4); + banks[..n].serialize(ser) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u32; MAX_BANKS], D::Error> { + let list = Vec::::deserialize(de)?; + if list.is_empty() || list.len() > MAX_BANKS { + return Err(serde::de::Error::custom(format!( + "banks: {} entries; give 1 to {MAX_BANKS} bank sizes in MB", list.len() + ))); + } + let mut banks = [0u32; MAX_BANKS]; + banks[..list.len()].copy_from_slice(&list); + Ok(banks) + } } fn default_scale() -> u32 { 1 } @@ -1643,24 +1699,90 @@ impl MachineConfig { } /// Translate keys an older iris wrote into their current form, saying so - /// once. `o2.disks` becomes `[scsi.N]` entries with `controller` = bus; - /// the list replaces the `scsi` map, since a config that has it never - /// meant the Indy's default disks. + /// once. The old `[o2]` spellings move to the shared keys they stand for + /// when those are still at their defaults (a shared key that is set wins); + /// `o2.disks` becomes `[scsi.N]` entries with `controller` = bus and + /// replaces the `scsi` map, since a config that has it never meant the + /// Indy's default disks. pub fn fold_deprecated(&mut self) { - if self.o2.disks.is_empty() { - return; + let mut moved: Vec<&str> = Vec::new(); + if let Some(prom) = self.o2.prom.take() { + if self.prom == DEFAULT_PROM { self.prom = prom; } + moved.push("o2.prom -> prom"); } - eprintln!("iris: `o2.disks` is deprecated; list the O2's disks as [scsi.N] with controller = bus (0 internal, 1 external)"); - self.scsi.clear(); - for d in std::mem::take(&mut self.o2.disks) { - self.scsi.insert(d.id, ScsiDeviceConfig { - path: d.path, - cdrom: d.cdrom, - overlay: d.overlay, - controller: d.bus, - ..Default::default() - }); + if let Some(flash) = self.o2.flash.take() { + if self.nvram == DEFAULT_NVRAM { self.nvram = flash; } + moved.push("o2.flash -> nvram"); + } + if let Some(mb) = self.o2.ram_mb.take() { + match crate::dev::crime_mem::kits_for(mb) { + Ok(kits) if self.banks == default_banks() => { + self.banks = [0; MAX_BANKS]; + self.banks[..kits.len()].copy_from_slice(&kits); + } + Ok(_) => {} + Err(e) => eprintln!("iris: o2.ram_mb = {mb}: {e}; ignored"), + } + moved.push("o2.ram_mb -> banks"); + } + if let Some(kits) = self.o2.dimms.take() { + if self.banks == default_banks() && !kits.is_empty() && kits.len() <= MAX_BANKS { + self.banks = [0; MAX_BANKS]; + self.banks[..kits.len()].copy_from_slice(&kits); + } + moved.push("o2.dimms -> banks"); + } + if !self.o2.disks.is_empty() { + self.scsi.clear(); + for d in std::mem::take(&mut self.o2.disks) { + self.scsi.insert(d.id, ScsiDeviceConfig { + path: d.path, + cdrom: d.cdrom, + overlay: d.overlay, + controller: d.bus, + ..Default::default() + }); + } + moved.push("o2.disks -> [scsi.N] (controller = bus)"); } + if let Some(port) = self.o2.serial_port.take() { + if self.serial_port_a.is_none() { self.serial_port_a = Some(port); } + moved.push("o2.serial_port -> serial_port_a"); + } + if let Some(log) = self.o2.serial_log.take() { + if self.serial_log.is_none() { self.serial_log = Some(log); } + moved.push("o2.serial_log -> serial_log"); + } + if let Some(graphics) = self.o2.graphics.take() { + if !graphics { self.headless = true; } + moved.push("o2.graphics -> headless (inverted)"); + } + if let Some(mhz) = self.o2.cpu_mhz.take() { + if self.clock.fixed_mhz.is_none() { self.clock.fixed_mhz = Some(mhz as f64 / 2.0); } + moved.push("o2.cpu_mhz -> [clock] fixed_mhz (half: CP0 Count)"); + } + if let Some(net) = self.o2.net.take() { + if !net { self.network.mode = NetMode::Off; } + moved.push("o2.net -> [network] mode = \"off\""); + } + if !self.o2.forwards.is_empty() { + let forwards = std::mem::take(&mut self.o2.forwards); + self.port_forward.extend(forwards); + moved.push("o2.forwards -> [[port_forward]]"); + } + if !moved.is_empty() { + eprintln!( + "iris: deprecated [o2] keys folded into the shared settings ({}); move them in the config to silence this", + moved.join(", ") + ); + } + } + + /// The O2's memory as the kit in each DIMM pair from pair A: `banks` with + /// the trailing zeros dropped. + pub fn o2_kits(&self) -> Vec { + let n = self.banks.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1); + self.banks[..n].to_vec() } /// Validate bank sizes, returns a description of any errors. @@ -1723,21 +1845,49 @@ impl MachineConfig { if self.scale < 1 || self.scale > 4 { return Err(format!("scale {} is invalid (valid: 1, 2, 3, 4)", self.scale)); } - for (i, &sz) in self.banks.iter().enumerate() { - if !VALID_BANK_SIZES.contains(&sz) { + let bank_count = self.machine.profile.memory_banks(); + for (i, &sz) in self.banks.iter().enumerate().skip(bank_count) { + if sz != 0 { return Err(format!( - "bank{} size {} MB is invalid (valid: {:?})", - i, sz, VALID_BANK_SIZES + "bank{} size {} MB: {:?} has {} memory banks, so banks beyond the {}th must be 0", + i, sz, self.machine.profile, bank_count, bank_count )); } - // The IP22/IP24 MC cannot express banks larger than 128 MB at its base - // shift; only the IP28's can. - if sz > 128 && !self.machine.profile.ip28() { - return Err(format!( - "bank{} size {} MB needs the IP28 (machine.profile = indigo2_ip28)", - i, sz - )); + } + if self.machine.profile == MachineProfile::O2Ip32 { + // A bank is the kit in a DIMM pair, filled from pair A. + if let Some(gap) = self.banks[..bank_count].iter().position(|&b| b == 0) { + if self.banks[gap..bank_count].iter().any(|&b| b != 0) { + return Err(format!("banks: DIMM pair {} is empty but a later pair is populated; fill the pairs from A", (b'A' + gap as u8) as char)); + } } + crate::dev::crime_mem::check_kits(&self.o2_kits()).map_err(|e| format!("banks: {e}"))?; + } else { + for (i, &sz) in self.banks.iter().enumerate().take(bank_count) { + if !VALID_BANK_SIZES.contains(&sz) { + return Err(format!( + "bank{} size {} MB is invalid (valid: {:?})", + i, sz, VALID_BANK_SIZES + )); + } + // The IP22/IP24 MC cannot express banks larger than 128 MB at its base + // shift; only the IP28's can. + if sz > 128 && !self.machine.profile.ip28() { + return Err(format!( + "bank{} size {} MB needs the IP28 (machine.profile = indigo2_ip28)", + i, sz + )); + } + } + } + match (self.network.mode, self.machine.profile) { + (NetMode::Off, p) if p != MachineProfile::O2Ip32 => { + return Err("network.mode = \"off\" is only available on the O2 for now; the Indy and Indigo2 always have their Ethernet".into()); + } + (NetMode::Pcap, MachineProfile::O2Ip32) => { + return Err("network.mode = \"pcap\" is not available on the O2 yet (its Ethernet runs on the NAT gateway)".into()); + } + _ => {} } if let Some(ref s) = self.nat_subnet { if let Err(e) = parse_nat_subnet(s) { @@ -2365,7 +2515,7 @@ mod export_tests { #[test] fn two_512_mb_banks_require_ip28() { let mut cfg = MachineConfig::default(); - cfg.banks = [512, 512, 0, 0]; + cfg.banks = banks_from4([512, 512, 0, 0]); for profile in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22] { cfg.machine.profile = profile; assert!(cfg.validate().unwrap_err().contains("512 MB needs the IP28")); @@ -2379,7 +2529,7 @@ mod export_tests { fn a_256_mb_bank_is_an_ip28_bank() { let mut cfg = MachineConfig::default(); cfg.machine.profile = MachineProfile::Indigo2Ip22; - cfg.banks = [256, 128, 0, 0]; + cfg.banks = banks_from4([256, 128, 0, 0]); let err = cfg.validate().expect_err("the IP22 MC cannot express a 256 MB bank"); assert!(err.contains("256 MB"), "{err}"); { @@ -2408,6 +2558,96 @@ mod export_tests { assert!(cfg.validate().unwrap_err().contains("daynaport")); } + /// `banks` reads one to eight entries and pads; it writes four or more + /// with no trailing zeros past the fourth; banks a machine does not have + /// must be 0; the O2 takes DIMM kits, largest first, and nothing else. + #[test] + fn banks_have_eight_slots_and_four_users() { + let cfg: MachineConfig = toml::from_str("banks = [64, 64]").expect("short list pads"); + assert_eq!(cfg.banks, banks_from4([64, 64, 0, 0])); + let s = toml::to_string(&MachineConfig::default()).expect("serialize"); + assert!(s.contains("banks = [128, 128, 0, 0]"), "four entries, no more:\n{s}"); + assert!(toml::from_str::("banks = [8, 8, 8, 8, 8, 8, 8, 8, 8]").is_err(), "nine is too many"); + + let mut cfg = MachineConfig::default(); + cfg.banks[4] = 8; + assert!(cfg.validate().unwrap_err().contains("has 4 memory banks")); + + let mut o2 = MachineConfig::default(); + o2.machine.profile = MachineProfile::O2Ip32; + o2.machine.cpu = CpuModel::R10000; + o2.validate().expect("two 128 MB kits are an O2's 256 MB"); + o2.banks = banks_from4([256, 256, 256, 256]); + o2.validate().expect("1 GB"); + assert_eq!(o2.o2_kits(), vec![256, 256, 256, 256]); + o2.banks = banks_from4([8, 0, 0, 0]); + assert!(o2.validate().unwrap_err().contains("no 8 MB kit")); + o2.banks = banks_from4([128, 0, 128, 0]); + assert!(o2.validate().unwrap_err().contains("pair B is empty")); + o2.banks = banks_from4([64, 128, 0, 0]); + assert!(o2.validate().unwrap_err().contains("largest kit goes in pair A")); + o2.banks = default_banks(); + o2.network.mode = NetMode::Pcap; + assert!(o2.validate().unwrap_err().contains("pcap")); + o2.network.mode = NetMode::Off; + o2.validate().expect("an unplugged O2"); + let mut indy = MachineConfig::default(); + indy.network.mode = NetMode::Off; + assert!(indy.validate().unwrap_err().contains("only available on the O2")); + } + + /// Every old `[o2]` key lands on the shared key it stood for, and the + /// section keeps only what is the O2's alone. + #[test] + fn deprecated_o2_keys_fold_into_shared_settings() { + let mut cfg: MachineConfig = toml::from_str(r#" + [machine] + profile = "o2_ip32" + cpu = "r10000" + [o2] + prom = "o2prom.bin" + flash = "o2flash.bin" + ram_mb = 384 + serial_port = 2291 + serial_log = "o2.log" + graphics = false + cpu_mhz = 195 + net = false + mouse = false + control_port = 2293 + forwards = [ { proto = "tcp", host_port = 2292, guest_port = 22 } ] + "#).expect("parse"); + cfg.fold_deprecated(); + assert_eq!(cfg.prom, "o2prom.bin"); + assert_eq!(cfg.nvram, "o2flash.bin"); + assert_eq!(cfg.banks, banks_from4([256, 128, 0, 0]), "384 MB, largest kits first"); + assert_eq!(cfg.serial_port_a, Some(2291)); + assert_eq!(cfg.serial_log.as_deref(), Some("o2.log")); + assert!(cfg.headless); + assert_eq!(cfg.clock.fixed_mhz, Some(97.5)); + assert_eq!(cfg.network.mode, NetMode::Off); + assert_eq!(cfg.port_forward.len(), 1); + assert_eq!(cfg.port_forward[0].host_port, 2292); + assert!(!cfg.o2.mouse); + assert_eq!(cfg.o2.control_port, Some(2293)); + cfg.validate().expect("the folded config is a valid O2"); + let s = toml::to_string_pretty(&cfg).expect("serialize"); + for gone in ["ram_mb =", "cpu_mhz =", "flash =", "forwards =", "graphics =", "serial_port =", "net ="] { + assert!(!s.contains(gone), "{gone} must not be written back:\n{s}"); + } + + // A shared key that is set wins over the old spelling. + let mut cfg: MachineConfig = toml::from_str(r#" + prom = "mine.bin" + [machine] + profile = "o2_ip32" + [o2] + prom = "old.bin" + "#).expect("parse"); + cfg.fold_deprecated(); + assert_eq!(cfg.prom, "mine.bin"); + } + /// `[scsi.17]` is id 1 on controller 1, so one id can be on both /// controllers; `controller = 1` on a low key still works; a key that /// names a controller cannot be contradicted, two tables cannot name the diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index 4338a26f..3f9dc570 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -152,22 +152,43 @@ pub struct O2Machine { /// What an O2 snapshot's manifest names as its machine. const SNAPSHOT_MACHINE: &str = "o2_ip32"; +/// The PROM image tried when `prom` is left at the shared default: the last +/// release's, named as SGI shipped it. The O2 has no embedded PROM. +pub const O2_PROM_FILE: &str = "ip32prom.rev4.18.bin"; + +/// CP0 Count when `[clock] fixed_mhz` is unset: half of a 180 MHz CPU. +pub const O2_COUNT_MHZ: f64 = 90.0; + impl O2Machine { pub fn new(cfg: &MachineConfig) -> Result { let o2 = &cfg.o2; - let prom = std::fs::read(&o2.prom).map_err(|e| format!("O2 PROM {}: {e}", o2.prom))?; - let kits = match &o2.dimms { - Some(k) => k.clone(), - None => crate::dev::crime_mem::kits_for(o2.ram_mb)?, + // `prom`, or the last release's file when `prom` is the shared default + // and that file is not there: there is no embedded O2 PROM. + let prom_path = if cfg.prom != crate::config::DEFAULT_PROM || std::path::Path::new(&cfg.prom).exists() { + cfg.prom.clone() + } else { + O2_PROM_FILE.to_string() }; + let prom = std::fs::read(&prom_path) + .map_err(|e| format!("O2 PROM {prom_path}: {e} (set `prom` to the O2 PROM image; 4.18 is the last release)"))?; + // `banks`: the kit in each DIMM pair from A (validate checked them). + let kits = cfg.o2_kits(); let mem = O2Memory::from_kits(&kits)?; - let opts = Ip32Options { scsi: true, flash_file: o2.flash.clone().map(Into::into) }; + // The PROM's flash, and so its environment, lives in `nvram`. + let opts = Ip32Options { scsi: true, flash_file: Some(cfg.nvram.clone().into()) }; let mut bus = Ip32Bus::with_options(mem, prom, opts); + // `[network] mac` is the address in the 1-Wire ID chip. + if let Some(mac) = cfg.network.mac.as_deref() { + let mac = crate::config::parse_mac(mac).map_err(|e| format!("network.mac: {e}"))?; + bus.mace.onewire = crate::dev::onewire::OneWireId::from_mac(mac); + } // MACE audio plays through the host unless audio is off. if !cfg.no_audio { bus.mace.audio.enable_output(cfg.audio.clone()); } - if o2.graphics { + // `headless`: no keyboard, so the PROM and the OS take the serial + // console, as on an O2 booted without one. + if !cfg.headless { bus.enable_graphics_with_mouse(o2.mouse); } let bus = Arc::new(bus); @@ -224,13 +245,16 @@ impl O2Machine { "iris: O2 with {} MB ({}), {}", kits.iter().sum::(), kits.iter().map(|k| format!("{k}")).collect::>().join("+"), - if o2.graphics { "graphics console" } else { "serial console" } + if cfg.headless { "serial console" } else { "graphics console" } ); - // The console UART's host end: the shared TCP serial backend (any - // number of clients), teed to the serial log when one is set. + // The console UART's host end: in CI mode the in-process backend + // the CI socket types into, otherwise TCP on `serial_port_a` (as the + // Indy's channel A), either teed to `serial_log` when one is set. The + // second port serves TCP on `serial_port_b`. + let [port_a, port_b] = cfg.serial_ports(); let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::z85c30::CiSerialBackend::new())); if let Some(ci) = &ci_serial { - if let Some(path) = cfg.serial_log.as_deref().or(o2.serial_log.as_deref()) { + if let Some(path) = cfg.serial_log.as_deref() { match ci.set_log_file(path) { Ok(()) => eprintln!("iris: O2 serial console mirroring to {path}"), Err(e) => eprintln!("iris: O2 serial log {path}: {e}"), @@ -239,16 +263,14 @@ impl O2Machine { bus.mace.com0.set_backend(ci.clone()); } else { let mut backend = crate::dev::z85c30::null_serial_backend(); - if let Some(port) = o2.serial_port { - match crate::dev::z85c30::tcp_serial_backend(port) { - Some((b, p)) => { - eprintln!("iris: O2 serial console on 127.0.0.1:{p}"); - backend = b; - } - None => eprintln!("iris: O2 serial console port {port} unavailable; is another iris using it?"), + match crate::dev::z85c30::tcp_serial_backend(port_a) { + Some((b, p)) => { + eprintln!("iris: O2 serial port 1 on 127.0.0.1:{p}"); + backend = b; } + None => eprintln!("iris: O2 serial port 1: port {port_a} unavailable; is another iris using it? (serial_port_a)"), } - if let Some(path) = &o2.serial_log { + if let Some(path) = &cfg.serial_log { // A fresh log each boot, as the O2 has always kept it (the // tee appends): scripts look at its tail for this boot's // prompts. @@ -260,12 +282,18 @@ impl O2Machine { } bus.mace.com0.set_backend(backend); } + match crate::dev::z85c30::tcp_serial_backend(port_b) { + Some((b, p)) => { + eprintln!("iris: O2 serial port 2 on 127.0.0.1:{p}"); + bus.mace.com1.set_backend(b); + } + None => eprintln!("iris: O2 serial port 2: port {port_b} unavailable; is another iris using it? (serial_port_b)"), + } + // The Ethernet on the NAT gateway `[network]` describes (`mode = + // "off"` leaves it unplugged; validate refuses pcap here for now). let mut nat = None; - if o2.net { - let mut net = crate::net::GatewayConfig::default(); - net.port_forwards = o2.forwards.clone(); - // `[network] tftp_dir`, so the PROM can `boot -f bootp()FILE`. - net.tftp_dir = cfg.network.tftp_dir.as_ref().map(std::path::PathBuf::from); + if cfg.network.mode != crate::config::NetMode::Off { + let net = crate::net::GatewayConfig::from_network(&cfg.network()); eprintln!("iris: O2 Ethernet on NAT: gateway {}, guest {} (DHCP)", net.gateway_ip, net.client_ip); let addrs = (net.client_ip, net.gateway_ip, net.netmask); nat = Some((bus.mace.mec.start_nat(net), addrs)); @@ -274,6 +302,10 @@ impl O2Machine { let timer_manager = Arc::new(TimerManager::new()); let sysad: Arc = bus.clone(); let arcs_fpu = o2.arcs_fpu; + // `[clock] fixed_mhz` is CP0 Count; the CPU clock the PROM measures + // and reports is twice it. + let count_mhz = cfg.clock.fixed_mhz.unwrap_or(O2_COUNT_MHZ); + let cpu_mhz = (count_mhz * 2.0).round().max(1.0) as u32; let boot = Arc::new(BootState::new()); let cpu: Arc = match cfg.machine.cpu { // The full cache model through the PROM and the tagless shadow @@ -281,8 +313,8 @@ impl O2Machine { // through the ppmem window with no tag probe, LRU or dirty bit on // every load and store (memory-bound code ran 2.5-4x slower than on // IP28 with the full model). - CpuModel::R10000 => Self::build_cpu::(sysad, &timer_manager, arcs_fpu, o2.cpu_mhz, boot.clone(), switch_model, HandoverCache::take_reset_seen), - CpuModel::R5000 => Self::build_cpu::(sysad, &timer_manager, arcs_fpu, o2.cpu_mhz, boot.clone(), switch_model, HandoverCache::take_reset_seen), + CpuModel::R10000 => Self::build_cpu::(sysad, &timer_manager, arcs_fpu, cpu_mhz, boot.clone(), switch_model, HandoverCache::take_reset_seen), + CpuModel::R5000 => Self::build_cpu::(sysad, &timer_manager, arcs_fpu, cpu_mhz, boot.clone(), switch_model, HandoverCache::take_reset_seen), CpuModel::R4400 => return Err("an O2 has an R5000 or an R10000 ([machine] cpu)".into()), }; @@ -332,7 +364,7 @@ impl O2Machine { bus.mace.ust.use_host_clock(); bus.connect_cpu_irq(crate::irq::IrqLine::new(Arc::new(CpuIrq(cpu.clone().as_device())), 2)); - bus.gbe.use_cpu_clock(cpu.cycles_ptr(), o2.cpu_mhz); + bus.gbe.use_cpu_clock(cpu.cycles_ptr(), cpu_mhz); // CRIME's rendering engine draws on its own thread, fed through // its interface buffer. bus.crime.re.start(bus.clone(), |b: &Ip32Bus| &b.crime.re); @@ -393,7 +425,7 @@ impl O2Machine { paused: Arc::new(AtomicBool::new(false)), lifecycle: crate::machine_state::GuestState::Idle, disks, - flash_path: o2.flash.clone(), + flash_path: Some(cfg.nvram.clone()), scratch_path, }) } diff --git a/src/machine.rs b/src/machine.rs index 835a3c7c..049a2297 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -304,8 +304,9 @@ impl Machine { // 1. Create all devices first // Memory Controller + let mc_banks: [u32; 4] = cfg.banks[..4].try_into().expect("the MC has four banks"); let mc = MemoryController::new_for_profile( - eeprom_mc.clone(), guinness, cfg.banks, cfg.machine.profile.ip28()); + eeprom_mc.clone(), guinness, mc_banks, cfg.machine.profile.ip28()); // IP28: IRIX takes the CPU speed from the PROM's `cpufreq` (194, i.e. // a 195 MHz R10000) instead of measuring it, and times with it: UST // assumes Count at half that, and the RPSS cycle counter the MC clock diff --git a/src/net/mod.rs b/src/net/mod.rs index d53c6687..492bd3de 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -117,6 +117,27 @@ impl Default for GatewayConfig { } } +impl GatewayConfig { + /// The gateway `[network]`, `nat_subnet`, `[nfs]` and `[[port_forward]]` + /// describe, as the machines hand it to their Ethernet. + pub fn from_network(net: &crate::config::NetworkConfig) -> Self { + let subnet = net.nat_subnet.clone().unwrap_or_default(); + Self { + gateway_ip: subnet.gateway_ip, + client_ip: subnet.client_ip, + netmask: subnet.netmask, + nfs: net.nfs.clone(), + port_forwards: net.port_forward.clone(), + mode: net.mode, + pcap_interface: net.pcap_interface.clone(), + nfs_pcap_ip: net.nfs_pcap_ip, + tftp_dir: net.tftp_dir.clone(), + rsh_dir: net.rsh_dir.clone(), + ..Self::default() + } + } +} + // ── Network backend abstraction ─────────────────────────────────────────────── // // The SEEQ 8003 chip is backend-agnostic: it hands outbound Ethernet frames to a