From 782d260f7d1309a4d21b75517eb03761fc8dd7a2 Mon Sep 17 00:00:00 2001 From: technomancer Date: Sat, 24 Oct 2026 12:40:35 -0700 Subject: [PATCH] o2: configure the machine with the shared keys; banks has eight entries The O2 had its own spelling of everything in `[o2]`: prom, flash, ram_mb or dimms, serial_port, serial_log, graphics, cpu_mhz, net and forwards. The section now holds only what the O2 alone has (mouse, control_port, arcs_fpu); the rest is the shared configuration: prom the O2 PROM image (no embedded one; when `prom` is the default, ip32prom.rev4.18.bin is tried) nvram the PROM's flash, kept between runs by default now banks the kit in each DIMM pair, 32/64/128/256 MB, largest first, validated through CRIME's kit rules serial_port_a/b the two UARTs (the second is served for the first time), serial_log teed off the first headless serial console instead of the graphics console [clock] fixed_mhz is CP0 Count on every machine; the O2's default is 90 (a 180 MHz CPU, which the PROM reports) [network] mode, nat_subnet, [nfs], tftp_dir, rsh_dir, mac (which reaches the 1-Wire ID chip) and [[port_forward]], built by the new GatewayConfig::from_network NetMode gains `off` (no Ethernet), accepted only on the O2 for now; pcap is refused on the O2, which has no bridged backend yet. `banks` is [u32; 8] with room for machines with more than today's four banks (the Octane): a config may list one to eight entries and is written back with four unless a later bank is populated, so existing files keep their shape. Every profile reports four banks and the rest must be 0. The GUI, the bench runner and the MC take their four from it. The old `[o2]` keys still parse: load_toml folds each onto the shared key it stands for when that key is at its default, prints one note listing what moved, and never writes them back. Docs: FEATURES.md, HELP.md, iris.toml, docs/o2-ip32.md, CHANGELOG.md, the bring-up log's summary. Tests: bank slots and padding, the O2 kit and network rules, the fold of every deprecated key. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 14 + FEATURES.md | 55 ++-- HELP.md | 11 +- clanker/devices/ip32-o2-bringup-log.md | 14 +- docs/o2-ip32.md | 77 +++-- iris-gui/src/config_ui.rs | 4 +- iris-gui/src/dialogs/new_machine.rs | 15 +- iris-gui/src/main.rs | 8 +- iris-gui/src/ram.rs | 4 +- iris-gui/src/settings.rs | 6 +- iris.toml | 5 +- src/bench_runner.rs | 2 +- src/config.rs | 436 +++++++++++++++++++------ src/ip32_machine.rs | 86 +++-- src/machine.rs | 3 +- src/net/mod.rs | 21 ++ 16 files changed, 549 insertions(+), 212 deletions(-) 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