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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ hashes are given where a change is easiest to understand by reading the commit.

## November 2026

### O2 PCI add-in cards

- The O2's one add-in slot (PCI device 3) takes a card from the configuration:
`[[pci]]` with `slot`, `card` and the card's options inline, or `--pci
[BUS:]SLOT=CARD[,KEY=VALUE]...`. Only a real add-in slot can be named; the
onboard AIC-7880s cannot. A mistake stops iris before it starts and the
message names the entry. The slot's INTA reaches CRIME input 10 and INTB to INTD
the shared inputs 13 to 15, as the BSDs and Linux route them.
- Two cards: `aic7880` (the onboard Adaptec chip, one SCSI bus) and
`isp12160` (QLogic, two). A card's buses are the SCSI controllers after the
onboard two, for `[scsi.N]` (key 16×controller+id, ids 1–15). IRIX 6.5.30
drives the `aic7880` card and `hinv` lists it with its disk; it finds the
`isp12160` but has no driver for it.
- The monitor's `pci slots` lists the bus's devices, onboard or add-in, with
what is fitted and where each interrupt pin goes.
- Snapshots record the fitted cards and refuse a restore into a machine with
others. Snapshots without cards are unchanged.
- The Octane's Bridge has a slot table with its four onboard devices and no
add-in slot: its PCI boards are not modelled.

### O2 graphics tracing

- `re trace FILE [re,cpu,gbe,crime,irq|all]` (also `gbe trace`, `crime trace`,
Expand Down Expand Up @@ -203,6 +223,13 @@ hashes are given where a change is easiest to understand by reading the commit.
- Memory past 4 GB is as fast as the rest: such a machine gets a 16 GB direct
window, and the JIT compiles code anywhere in it.
- A store-conditional on memory outside the window is now atomic with two CPUs.
### PCI configuration

- A PCI function can describe its own writable header fields and
write-one-to-clear status bits. Byte and halfword configuration writes
through a Bridge carry their byte enables, so such a function keeps the
status events a narrow write did not select. No Octane function has
write-one-to-clear status bits, so the Octane behaves as before.

### VNC, and screen and pointer commands

Expand Down
16 changes: 16 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,21 @@ Omit `[nfs]` to disable the share; the default forward list is empty.
| `port_forward[].guest_port` | `required` | Guest destination port. |
| `port_forward[].bind` | `localhost` | localhost or any; any listens on all interfaces. |

### [[pci]]

PCI add-in cards, one table per card. Only a real add-in slot can be named: the onboard devices are fixed.
Today that is the O2's one slot (PCI device 3, bus `pci`); the Octane's PCI boards are not modelled.
The monitor's `pci slots` lists a machine's slots.

| Key | Default | Values and behavior |
|---|---|---|
| `pci[].slot` | `required` | The PCI device number (`3`, a number, not text), or the label printed on the chassis as text (`"left"`) where the slot has one. Labels work in the configuration file only; `--pci` takes device numbers. The O2's slot has no label. |
| `pci[].card` | `required` | `aic7880`: Adaptec AIC-7880 SCSI, one bus. `isp12160`: dual-channel QLogic ISP12160 SCSI, two buses. A card's buses are the SCSI controllers after the machine's own (2, then 3 on the O2; the next card's follow), for `[scsi.N]` keys 16×controller+id, ids 1–15. IRIX 6.5.30 on the O2 drives the `aic7880` card and lists its disks in `hinv`; it finds the `isp12160` on the bus but has no driver for it. |
| `pci[].bus` | the machine's only bus | The PCI bus id (`pci` on the O2). Needed only where a machine has several. |
| `pci[].<other>` | none | The card's own options, inline. `isp12160` takes none. |

Several cards of one kind are allowed where there are slots for them. A mistake (an onboard device, an unknown slot, a slot used twice, an unknown card or option) stops iris before it starts, with a message naming the entry. A snapshot records the fitted cards and loads only into a machine with the same ones.

### [vino]

| Key | Default | Values and behavior |
Expand Down Expand Up @@ -427,6 +442,7 @@ iris-gui does not parse this standalone option set; use its named-machine UI.
| `--list-net-interfaces` | Print the host network interfaces libpcap can bridge onto, then exit. Requires a build with --features pcap. |
| `--no-scsi-deferred-int` | Disable deferred SCSI status interrupts (default: enabled for OpenBSD/NetBSD compatibility). |
| `--cpu` | Emulated CPU: `r4400` (default), `r5000`, or `r10000`. Overrides `[machine] cpu`. |
| `--pci` | Fit a PCI card: `[BUS:]SLOT=CARD[,KEY=VALUE]...`, for example `--pci 3=aic7880` on the O2. Repeatable; replaces a `[[pci]]` entry for the same slot. |
| `--graphics` | Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`), `v6`, `v8`, `v10`, `v12` (`vpro:v12` and so on, Octane only). |
| `--gdb-port` | Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). Connect with: target remote localhost:<port> |
| `--monitor-port` | Monitor console port on 127.0.0.1 (default 8888). Give each iris running at once its own. |
Expand Down
6 changes: 6 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ way: `scsi::attach_from_config` walks the `[scsi.N]` tables for every machine,
where the key is a slot (controller × 16 + id, see `config::scsi_slot`), and
the machine supplies only a route from controller number to bus.

`ScsiTarget::bus_reset` defaults to no action. Disk targets record reset as
unit attention. Only the ISP12160 calls it; the other controllers keep
their earlier behaviour of not reporting resets to targets.
The ISP12160 releases its controller state lock before walking either bus.
Target callbacks run under that bus lock and must not reacquire it.

## 2. Data path and endianness

**Word-transparent architecture.** Host `u32`/`u64` values are bit
Expand Down
10 changes: 10 additions & 0 deletions HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,15 @@ path = "cdrom4.iso"
cdrom = true
# discs = ["irix65.iso", "extras.iso", "patches.iso"] # changer; "scsi eject 4" cycles

# PCI add-in cards. The O2 has one slot, PCI device 3. An "aic7880" card adds one SCSI
# bus, controller 2: [scsi.33] is id 1 on it. An "isp12160" adds two (controllers 2 and 3;
# IRIX on the O2 has no driver for it). The monitor's `pci slots` lists the slots.
# [[pci]]
# slot = 3
# card = "aic7880"
# [scsi.33]
# path = "card-disk.raw"

# Scratch volume for file injection without networking (see README).
# [scsi.2]
# path = "scratch.raw"
Expand Down Expand Up @@ -786,6 +795,7 @@ Configuration
--nveeprom <FILE> Indigo2 NVRAM EEPROM file (default: nveeprom.bin)
--machine <NAME> Machine to emulate (a [machine] profile name); `help` lists them
--ip22 Same as --machine indigo2_ip22
--pci <[BUS:]SLOT=CARD[,K=V]...> Fit a PCI add-in card, e.g. 3=aic7880 on the O2 (repeatable)
--graphics <BOARD> newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12
--cpu <MODEL> r4400 (default), r5000, or r10000
--bank0..--bank3 <MB> RAM bank sizes (0/8/16/32/64/128; IP28: also 256/512)
Expand Down
2 changes: 1 addition & 1 deletion debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ its SCSI controllers answer to `scsi`/`cow` and `scsi1`/`cow1` as described unde
| `re dither [on\|off]` | Show or set whether the engine dithers, to compare renderings; on by default |
| `gbe status` | GBE display engine: timing, frame buffer, tiles |
| `isa status` | MACE ISA interrupts |
| `pci status` / `pci config <slot>` | MACE PCI bridge / a function's configuration space |
| `pci status` / `pci slots` / `pci config <slot>` | MACE PCI bridge / each device number the bus table knows, onboard or add-in, with what is in it and where its interrupt pins go / a function's configuration space |
| `mec status` | MACE Ethernet: rings, receive buffers, frame counts |
| `sdma status` | MACE serial DMA |
| `maceps2 status` / `maceps2 traffic` | PS/2 ports / recent bytes each way |
Expand Down
18 changes: 17 additions & 1 deletion docs/o2-ip32.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,30 @@ overlay } ]`. That key is deprecated: iris folds it into `[scsi.N]` (with
`controller = bus`) when it loads the file and says so; move the entries over
to silence the note.

## PCI add-in cards

The O2 has one add-in slot, PCI device 3 on the MACE bus, and `[[pci]]` (or
`--pci 3=aic7880`) fits a card in it ([FEATURES.md](../FEATURES.md#pci)). The
slot's INTA reaches CRIME input 10 and INTB to INTD reach the shared inputs
13 to 15, as the MACE specification has them (and Linux, OpenBSD and NetBSD route them); what the board's connector
wires INTB to INTD to is not known. Devices 1 and 2 are the onboard AIC-7880s and cannot be replaced. The
cards are the Adaptec AIC-7880 (`aic7880`, one SCSI bus) and the QLogic
ISP12160 (`isp12160`, two). A card's buses are SCSI controllers 2 onwards: put
disks on them with `[scsi.N]` keys 16×controller+id. IRIX 6.5.30 drives the
`aic7880` card, and `hinv` lists "PCI SCSI controller 2" with its disk; it finds
the `isp12160` on the bus ("PCI Adapter ID ... PCI slot 3" in `hinv -v`) but
has no driver for it. A card's buses have no monitor `scsi` command yet; set
disks in the configuration.
`pci slots` in the monitor shows the slot and what is in it.

## Snapshots

The monitor's `save NAME` and `load NAME`, and iris-ci's `save` and
`restore`, keep the O2 in `saves/NAME/` as they do the other machines: the
CPU, every device, all of memory (in the chunk store the snapshots share, so
memory that did not change costs nothing), the flash, and the changes on
`overlay` disks. A snapshot loads only into an O2 with the same CPU, memory,
disks and build. Use `overlay` disks for snapshots you mean to go back to: a
disks, PCI cards and build. Use `overlay` disks for snapshots you mean to go back to: a
disk written in place keeps changing after the save, and the saved memory
will not match it.

Expand Down
104 changes: 101 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use clap::{CommandFactory, FromArgMatches, Parser};
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;

use crate::pci_addin::PciEntry;

/// Every memory bank size in MB, for the front-ends' menus; `bank_sizes` says
/// which of them a machine takes.
pub const VALID_BANK_SIZES: &[u32] = &[0, 8, 16, 32, 64, 128, 256, 512, 1024, 2048];
Expand Down Expand Up @@ -1819,6 +1821,13 @@ pub struct MachineConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub port_forward: Vec<PortForwardConfig>,

/// PCI add-in cards, one `[[pci]]` table per card: `slot` (a PCI device
/// number or the chassis label), `card`, `bus` where the machine has
/// several, and the card's own options. Only a real add-in slot can be
/// named; the onboard devices are not configurable.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pci: Vec<PciEntry>,

/// VINO video-in configuration (IndyCam emulation source).
#[serde(default)]
pub vino: VinoConfig,
Expand Down Expand Up @@ -2054,6 +2063,7 @@ impl Default for MachineConfig {
scale: default_scale(),
nfs: None,
port_forward: vec![],
pci: vec![],
headless: false,
no_audio: false,
gdb_port: None,
Expand Down Expand Up @@ -2326,13 +2336,18 @@ impl MachineConfig {
return Err("graphics.resolution presets require Newport (graphics.board = newport)".into());
}
}
// The cards' SCSI buses come after the machine's own controllers.
let cards = crate::pci_addin::validate_pci(self.machine.profile, &self.pci)?;
let own = self.machine.profile.scsi_controllers();
let controllers = cards.iter().fold(own, |n, c| n.saturating_add(c.card.scsi_controllers));
let mut placed: std::collections::HashMap<(u8, u8), u8> = std::collections::HashMap::new();
let mut keys: Vec<u8> = self.scsi.keys().copied().collect();
keys.sort();
for key in keys {
let dev = &self.scsi[&key];
let (controller, id) = dev.placement(key);
let max_id = self.machine.profile.max_scsi_id();
// A card's buses are wide, whatever the machine's own are.
let max_id = if controller >= own.max(2) { 15 } else { self.machine.profile.max_scsi_id() };
if id == 0 || id > max_id {
return Err(format!("scsi.{}: SCSI ID {} is out of range (1–{}; the controller itself is 0)", key, id, max_id));
}
Expand All @@ -2342,8 +2357,9 @@ impl MachineConfig {
key, controller, dev.controller
));
}
if controller > 1 {
return Err(format!("scsi.{}: controller {} is invalid (valid: 0, 1)", key, controller));
if controller >= controllers.max(2) {
let valid = if controllers <= 2 { "0, 1".to_string() } else { format!("0 to {}", controllers - 1) };
return Err(format!("scsi.{}: controller {} is invalid (valid: {valid})", key, controller));
}
if controller != 0 && self.machine.profile.scsi_controllers() < 2 {
return Err(format!(
Expand Down Expand Up @@ -2730,6 +2746,13 @@ pub struct Cli {
#[arg(long = "cache", value_name = "MODEL")]
pub cache: Option<CacheModel>,

/// Fit a PCI add-in card: `[BUS:]SLOT=CARD[,KEY=VALUE]...`, for example
/// `--pci 3=aic7880` on the O2. SLOT is a PCI device number (a chassis
/// label works in the configuration file only). Repeat it for more cards; it replaces a `[[pci]]` entry for the
/// same slot. The monitor's `pci slots` lists a machine's slots.
#[arg(long = "pci", value_name = "[BUS:]SLOT=CARD[,K=V]...", value_parser = PciEntry::parse_cli)]
pub pci: Vec<PciEntry>,

/// Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`).
#[arg(long = "graphics", visible_alias = "board", visible_alias = "graphics-board", value_name = "BOARD")]
pub graphics: Option<GraphicsBoard>,
Expand Down Expand Up @@ -2909,6 +2932,7 @@ impl Cli {
if let Some(cpu) = self.cpu { cfg.machine.cpu = cpu; }
if let Some(cache) = self.cache { cfg.machine.cache = cache; }
if let Some(b) = self.graphics { cfg.graphics.board = Some(b); }
crate::pci_addin::merge_cli(cfg.machine.profile, &mut cfg.pci, &self.pci);
if self.scale2x { cfg.scale = 2; }
if self.headless { cfg.headless = true; }
if self.no_audio { cfg.no_audio = true; }
Expand Down Expand Up @@ -3881,3 +3905,77 @@ mod ci_overlay_tests {
assert!(a.contains(&format!("iris-ci-{}-", std::process::id())), "{a}");
}
}

#[cfg(test)]
mod pci_tests {
use super::*;

/// `[[pci]]` reads and writes back inline, the card's own keys beside
/// `slot` and `card`, and a chassis label is as good as a device number.
#[test]
fn pci_entries_round_trip_through_toml() {
let text = "[machine]\nprofile = \"o2_ip32\"\ncpu = \"r10000\"\n\n[[pci]]\nslot = 3\ncard = \"isp12160\"\n";
let cfg: MachineConfig = toml::from_str(text).unwrap();
assert_eq!(cfg.pci.len(), 1);
assert_eq!((cfg.pci[0].slot.clone(), cfg.pci[0].card.as_str()), (crate::pci_addin::SlotRef::Device(3), "isp12160"));
cfg.validate().expect("the O2's slot takes the card");
let back: MachineConfig = toml::from_str(&toml::to_string(&cfg).unwrap()).unwrap();
assert_eq!(back.pci, cfg.pci);
let labelled: MachineConfig = toml::from_str("[[pci]]\nslot = \"left\"\ncard = \"isp12160\"\nbus = \"pci\"\nspeed = 2\n").unwrap();
assert_eq!(labelled.pci[0].slot, crate::pci_addin::SlotRef::Label("left".into()));
assert_eq!(labelled.pci[0].options["speed"], toml::Value::Integer(2), "other keys are the card's options");
assert_eq!(MachineConfig::default().pci.len(), 0);
assert!(!toml::to_string(&MachineConfig::default()).unwrap().contains("pci"), "an empty list is not written");
}

/// A bad card entry stops `validate` with the entry's own message, and
/// a machine with no PCI refuses any.
#[test]
fn validate_reports_a_bad_pci_entry() {
let mut cfg = MachineConfig::default();
cfg.machine.profile = MachineProfile::O2Ip32;
cfg.machine.cpu = CpuModel::R10000;
cfg.pci.push(PciEntry::parse_cli("2=isp12160").unwrap());
assert!(cfg.validate().unwrap_err().contains("is the onboard external SCSI controller"), "slot 2 is the second AIC-7880");
cfg.pci[0] = PciEntry::parse_cli("3=isp12160").unwrap();
cfg.validate().unwrap();
cfg.machine.profile = MachineProfile::IndyIp24;
cfg.machine.cpu = CpuModel::default();
assert_eq!(cfg.validate().unwrap_err(), "pci[0]: the Indy has no PCI bus.");
}

/// The card's two SCSI buses are controllers 2 and 3, and their targets
/// may use the wide bus's ids.
#[test]
fn a_card_adds_scsi_controllers_to_the_machines_own() {
let mut cfg = MachineConfig::default();
cfg.machine.profile = MachineProfile::O2Ip32;
cfg.machine.cpu = CpuModel::R10000;
cfg.scsi.insert(2 * 16 + 9, ScsiDeviceConfig { path: "card.raw".into(), ..Default::default() });
assert!(cfg.validate().unwrap_err().contains("controller 2 is invalid (valid: 0, 1)"), "no card, no controller 2");
cfg.pci.push(PciEntry::parse_cli("3=isp12160").unwrap());
cfg.validate().expect("controller 2, id 9: the card's first bus");
cfg.scsi.insert(3 * 16 + 1, ScsiDeviceConfig { path: "card2.raw".into(), ..Default::default() });
cfg.validate().expect("controller 3: the card's second bus");
cfg.scsi.insert(4 * 16 + 1, ScsiDeviceConfig { path: "x.raw".into(), ..Default::default() });
assert!(cfg.validate().unwrap_err().contains("controller 4 is invalid (valid: 0 to 3)"));
cfg.scsi.remove(&(4 * 16 + 1));
cfg.scsi.insert(1, ScsiDeviceConfig { path: "onboard.raw".into(), ..Default::default() });
cfg.scsi.insert(8, ScsiDeviceConfig { path: "onboard8.raw".into(), ..Default::default() });
assert!(cfg.validate().unwrap_err().contains("SCSI ID 8 is out of range (1–7"), "the O2's own buses stay narrow");
}

#[test]
fn the_pci_option_parses_and_replaces_a_config_entry() {
let cli = Cli::try_parse_from(["iris", "--pci", "pci:3=isp12160", "--pci", "4=isp12160"]).unwrap();
assert_eq!(cli.pci.len(), 2);
assert!(Cli::try_parse_from(["iris", "--pci", "nonsense"]).is_err());
let mut cfg = MachineConfig::default();
cfg.machine.profile = MachineProfile::O2Ip32;
cfg.pci.push(PciEntry::parse_cli("3=isp12160").unwrap());
let cfg = cli.apply(cfg);
assert_eq!(cfg.pci.len(), 2, "slot 3 replaced, 4 appended");
assert_eq!(cfg.pci[0].bus.as_deref(), Some("pci"));
}

}
10 changes: 4 additions & 6 deletions src/dev/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,12 +553,10 @@ impl Bridge {
pub fn attach(self: &Arc<Self>, slot: u8, dev: Arc<dyn PciBusDevice>) {
let slot = slot as usize;
assert!(slot < DEVICES, "Bridge has {DEVICES} slots");
dev.connect_dma(Arc::new(SlotDma { bridge: Arc::downgrade(self), slot }));
let pin = dev.config().interrupt_pin();
if (1..=4).contains(&pin) {
crate::pci::plug(&*dev, Arc::new(SlotDma { bridge: Arc::downgrade(self), slot }), |pin| {
let router = Arc::new(PciPin { bridge: Arc::downgrade(self) });
dev.connect_interrupt(IrqLine::new(router, (slot * 4 + pin as usize - 1) as u32));
}
IrqLine::new(router, (slot * 4 + pin as usize - 1) as u32)
});
self.slots.write()[slot] = Some(dev);
self.publish_bars();
}
Expand Down Expand Up @@ -1057,7 +1055,7 @@ impl Bridge {
let i = (reg >> 2) as usize;
let shift = 8 * (reg & 3);
let mask = (u64::MAX >> (64 - 8 * size)) << shift;
cfg.write(i, ((cfg.read(i) as u64 & !mask) | ((val << shift) & mask)) as u32);
cfg.write_masked(i, (val << shift) as u32, mask as u32);
true
}

Expand Down
Loading
Loading