From d3fe39ee48acb1e5e38117432a509ec053d84f58 Mon Sep 17 00:00:00 2001 From: James Braid Date: Mon, 2 Nov 2026 11:36:14 -0700 Subject: [PATCH 01/12] pci: attach a function through one helper MacePci::attach and Bridge::attach each repeated the same four steps: hand the function its way to memory, read its interrupt pin, build the line for that pin if there is one, and connect it. They differ only in the DMA object and in what a pin is wired to. pci::plug takes those two as arguments and does the rest, so a bus that gains a way to place a function at a device number does it in one place. No behaviour change: the same objects are connected in the same order, and a function with no pin is still left unconnected. --- src/dev/bridge.rs | 8 ++---- src/dev/mace_pci.rs | 8 ++---- src/pci.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index 41fcf4fa9..90d28122d 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -553,12 +553,10 @@ impl Bridge { pub fn attach(self: &Arc, slot: u8, dev: Arc) { 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(); } diff --git a/src/dev/mace_pci.rs b/src/dev/mace_pci.rs index ff1f41c91..b5a5a1b43 100644 --- a/src/dev/mace_pci.rs +++ b/src/dev/mace_pci.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex, OnceLock, RwLock}; use crate::irq::{InterruptController, IrqLine}; use crate::dev::crime_mem::O2Memory; -use crate::pci::{bar_runs, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; +use crate::pci::{bar_runs, plug, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; use crate::snapshot::{get_field, hex_u32, load_u32_slice, toml_u32, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead32, BusRead64, Saveable, BUS_OK}; use crate::ip32::*; @@ -296,11 +296,7 @@ impl MacePci { /// Put a function in a slot on bus 0, wire its interrupt pin to the /// bridge, which routes it on, and give it host memory to master. pub fn attach(&self, slot: u8, name: &'static str, dev: Arc) { - dev.connect_dma(self.dma.clone()); - let pin = dev.config().interrupt_pin(); - if (1..=4).contains(&pin) { - dev.connect_interrupt(IrqLine::new(self.irq.clone(), PciIrqRouter::line(slot, pin))); - } + plug(&*dev, self.dma.clone(), |pin| IrqLine::new(self.irq.clone(), PciIrqRouter::line(slot, pin))); assert!(self.funcs[slot as usize].set(dev.clone()).is_ok(), "pci: slot {slot} is taken"); let mut slots = self.slots.lock().unwrap(); slots.insert(slot, Arc::new(Slot { name, dev, touched: Mutex::new(Vec::new()) })); diff --git a/src/pci.rs b/src/pci.rs index f81627099..d96cf039e 100644 --- a/src/pci.rs +++ b/src/pci.rs @@ -300,6 +300,21 @@ pub trait PciDma: Send + Sync { fn dma_write(&self, addr: u64, data: &[u8]) -> PciDmaStatus; } +/// Seat a function on a bus: give it its way to memory and, when its header +/// names an interrupt pin (INTA..INTD), the line for that pin. +/// +/// Every host bridge does this by hand when a function is attached. They +/// differ only in the DMA object they hand out and in what a pin is wired +/// to, which is what the two arguments are. A function with no pin is +/// left unconnected. +pub fn plug(dev: &dyn PciBusDevice, dma: Arc, irq_for_pin: impl FnOnce(u8) -> IrqLine) { + dev.connect_dma(dma); + let pin = dev.config().interrupt_pin(); + if (1..=4).contains(&pin) { + dev.connect_interrupt(irq_for_pin(pin)); + } +} + // ---- BARs on the physical bus ---- /// What a host bridge does to a CPU access on its way to a function: which @@ -566,4 +581,57 @@ mod tests { assert!(bar_runs(&funcs, Space::Mem, 0x1a00_0000, 0x9000_0000, 0x200_0000, |_| unreachable!()).is_empty(), "outside the window"); } + /// A function that records what the bridge connected. + struct Wired { + cfg: PciConfig, + dma: Mutex, + irq: Mutex>, + } + + impl Wired { + fn new(pin: u8) -> Arc { + Arc::new(Self { cfg: PciConfig::new(0x1234, 0x5678, 0, 0, [Bar::Unused; 6], pin), dma: Mutex::new(false), irq: Mutex::new(None) }) + } + } + + impl PciBusDevice for Wired { + fn config(&self) -> &PciConfig { &self.cfg } + fn bar_read(&self, _: usize, _: u32, _: usize) -> u32 { 0 } + fn bar_write(&self, _: usize, _: u32, _: u32, _: usize) {} + fn connect_interrupt(&self, line: IrqLine) { *self.irq.lock().unwrap() = Some(line); } + fn connect_dma(&self, _: Arc) { *self.dma.lock().unwrap() = true; } + } + + struct NoMemory; + impl PciDma for NoMemory { + fn dma_read(&self, _: u64, _: &mut [u8]) -> PciDmaStatus { PciDmaStatus::MasterAbort } + fn dma_write(&self, _: u64, _: &[u8]) -> PciDmaStatus { PciDmaStatus::MasterAbort } + } + + use crate::irq::InterruptController; + struct Inputs(Mutex>); + impl InterruptController for Inputs { + fn set_line(&self, line: u32, level: bool) { self.0.lock().unwrap().push((line, level)); } + } + + /// Memory always; a line only for a pin INTA..INTD, and the line is the + /// one the bridge builds for that pin. + #[test] + fn plug_gives_memory_and_a_line_for_a_real_pin() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + for pin in 0..=5u8 { + let f = Wired::new(pin); + let mut asked = Vec::new(); + plug(&*f, Arc::new(NoMemory), |p| { asked.push(p); IrqLine::new(inputs.clone(), p as u32) }); + assert!(*f.dma.lock().unwrap(), "pin {pin} still masters the bus"); + let wired = f.irq.lock().unwrap().is_some(); + assert_eq!(wired, (1..=4).contains(&pin), "pin {pin}"); + assert_eq!(asked, if wired { vec![pin] } else { vec![] }, "pin {pin}: the bridge is asked only for a real pin"); + } + let f = Wired::new(PCI_INTA + 2); + plug(&*f, Arc::new(NoMemory), |p| IrqLine::new(inputs.clone(), 40 + p as u32)); + f.irq.lock().unwrap().as_ref().unwrap().set(true); + assert_eq!(*inputs.0.lock().unwrap(), vec![(43, true)]); + } + } From 6cc2b7c22b91bce919c2b6597332f7022508a239 Mon Sep 17 00:00:00 2001 From: James Braid Date: Mon, 2 Nov 2026 11:37:20 -0700 Subject: [PATCH 02/12] pci: move the O2's interrupt OR-routing into IntxTable MacePci kept each pin's level and ORed the pins routed to one CRIME input in a private router whose routing was a fn pointer baked in at construction. The Fuel and Tezro buses carry copies of the same logic. IntxTable is that logic as a type in pci.rs, with the routes held as data (IntxTable::route) instead of a function. A bus can then wire a function at a device number it learns about after construction without changing the bridge. MacePci fills the table from its existing routing function, so the O2 routes exactly what it did: slot 1 INTA to CRIME 8, slot 2 INTA to CRIME 9, everything else dropped. Levels, the snapshot "pins" array, the pci status line and the order in which a shared input is driven are unchanged. The table also drops the per-change scan that called the routing function for all 128 lines; the scan now reads the stored routes. --- src/dev/mace_pci.rs | 83 ++++++++++++++------------------- src/pci.rs | 111 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 50 deletions(-) diff --git a/src/dev/mace_pci.rs b/src/dev/mace_pci.rs index b5a5a1b43..ae70762ef 100644 --- a/src/dev/mace_pci.rs +++ b/src/dev/mace_pci.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex, OnceLock, RwLock}; use crate::irq::{InterruptController, IrqLine}; use crate::dev::crime_mem::O2Memory; -use crate::pci::{bar_runs, plug, BarAt, BarRun, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; +use crate::pci::{bar_runs, plug, BarAt, BarRun, IntxTable, BarRunSink, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; use crate::snapshot::{get_field, hex_u32, load_u32_slice, toml_u32, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead32, BusRead64, Saveable, BUS_OK}; use crate::ip32::*; @@ -48,48 +48,8 @@ fn crime_input(slot: u8, pin: u8) -> Option { } /// Bus 0 has 32 device numbers, each with four interrupt pins. -const PCI_PINS: usize = 32 * 4; - -/// The bridge's interrupt router. Each slot's INTA..INTD is a line -/// (`slot * 4 + pin - 1`); the router keeps every pin's level and drives -/// each system input it routes to with the OR of the pins routed there, so -/// on a shared input one function cannot clear another's interrupt. -struct PciIrqRouter { - /// Where the system inputs are: CRIME. - intc: Arc, - /// Slot and pin (1 = INTA) to system input. - route: fn(u8, u8) -> Option, - levels: Mutex<[bool; PCI_PINS]>, -} - -impl PciIrqRouter { - fn line(slot: u8, pin: u8) -> u32 { - slot as u32 * 4 + (pin as u32 - 1) - } - - fn slot_pin(line: u32) -> (u8, u8) { - ((line / 4) as u8, (line % 4) as u8 + 1) - } -} - -impl InterruptController for PciIrqRouter { - fn set_line(&self, line: u32, level: bool) { - if line as usize >= PCI_PINS { - return; - } - let (slot, pin) = Self::slot_pin(line); - let Some(input) = (self.route)(slot, pin) else { return }; - // The lock is held while CRIME is driven, so two pins sharing an - // input cannot reach it out of order. - let mut levels = self.levels.lock().unwrap(); - levels[line as usize] = level; - let any = (0..PCI_PINS as u32).any(|l| { - let (s, p) = Self::slot_pin(l); - levels[l as usize] && (self.route)(s, p) == Some(input) - }); - self.intc.set_line(input, any); - } -} +const PCI_SLOTS: usize = 32; +const PCI_PINS: usize = PCI_SLOTS * 4; /// The CPU-side window onto PCI memory, and the PCI address it starts at. /// @@ -261,7 +221,7 @@ pub struct MacePci { windows: [AtomicU64; MAX_WINDOWS], nwindows: AtomicUsize, /// The slots' interrupt pins, routed to CRIME. - irq: Arc, + irq: Arc, /// Host memory, for the functions that master the bus. dma: Arc, /// Where the runs of the windows a BAR decodes whole go (the machine @@ -278,6 +238,12 @@ impl MacePci { /// A bridge with its own routing of slot pins (1 = INTA) to `intc`'s /// inputs. fn with_routing(intc: Arc, route: fn(u8, u8) -> Option, ram: Arc) -> Self { + let irq = IntxTable::new(intc, PCI_SLOTS); + for slot in 0..PCI_SLOTS as u8 { + for pin in 1..=4 { + irq.route(slot, pin, route(slot, pin)); + } + } Self { config_addr: Mutex::new(0), regs: Mutex::new([0u32; (MACEPCI_SIZE / 4) as usize]), @@ -286,7 +252,7 @@ impl MacePci { funcs: std::array::from_fn(|_| OnceLock::new()), windows: std::array::from_fn(|_| AtomicU64::new(0)), nwindows: AtomicUsize::new(0), - irq: Arc::new(PciIrqRouter { intc, route, levels: Mutex::new([false; PCI_PINS]) }), + irq: Arc::new(irq), dma: Arc::new(HostMemory::new(ram)), bar_sink: RwLock::new(None), bar_entries: Mutex::new(Default::default()), @@ -296,7 +262,7 @@ impl MacePci { /// Put a function in a slot on bus 0, wire its interrupt pin to the /// bridge, which routes it on, and give it host memory to master. pub fn attach(&self, slot: u8, name: &'static str, dev: Arc) { - plug(&*dev, self.dma.clone(), |pin| IrqLine::new(self.irq.clone(), PciIrqRouter::line(slot, pin))); + plug(&*dev, self.dma.clone(), |pin| IrqLine::new(self.irq.clone(), IntxTable::line(slot, pin))); assert!(self.funcs[slot as usize].set(dev.clone()).is_ok(), "pci: slot {slot} is taken"); let mut slots = self.slots.lock().unwrap(); slots.insert(slot, Arc::new(Slot { name, dev, touched: Mutex::new(Vec::new()) })); @@ -575,7 +541,7 @@ impl Saveable for MacePci { let mut t = toml::map::Map::new(); t.insert("config_addr".into(), hex_u32(*self.config_addr.lock().unwrap())); t.insert("regs".into(), u32_slice_to_toml(&*self.regs.lock().unwrap())); - let levels = *self.irq.levels.lock().unwrap(); + let levels = self.irq.levels(); t.insert("pins".into(), toml::Value::Array(levels.iter().map(|&b| toml::Value::Boolean(b)).collect())); let mut slots = toml::map::Map::new(); for (n, s) in self.slots.lock().unwrap().iter() { @@ -637,7 +603,7 @@ impl crate::traits::Device for MacePci { let bars: Vec = (0..6).map(|i| c.bar_base(i)).filter(|&b| b != 0).map(|b| format!("{b:#x}")).collect(); let irq = match c.interrupt_pin() { 0 => "no interrupt".to_string(), - p => match (self.irq.route)(*slot, p) { + p => match self.irq.input(*slot, p) { Some(l) => format!("INT{} to CRIME {l}", (b'A' + p - 1) as char), None => format!("INT{} unrouted", (b'A' + p - 1) as char), }, @@ -856,4 +822,25 @@ mod tests { lb.set(false); assert_eq!(last(), (12, false)); } + + /// Both onboard SCSI controllers' INTA reach their own CRIME inputs, 8 + /// and 9, and nothing else does. + #[test] + fn the_onboard_scsi_pins_reach_crime_8_and_9() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime.clone(), test_ram()); + let fake = |pin| Arc::new(Fake { + cfg: PciConfig::new(0x9004, 0x8078, 0x0100_00, 1, [Bar::Unused; 6], pin), + last: Mutex::new(None), + irq: std::sync::OnceLock::new(), + }); + let (a, b, c) = (fake(PCI_INTA), fake(PCI_INTA), fake(PCI_INTA + 1)); + pci.attach(IP32_SCSI_SLOT, "a", a.clone()); + pci.attach(IP32_SCSI_SLOT_B, "b", b.clone()); + pci.attach(3, "c", c.clone()); + a.irq.get().unwrap().set(true); + b.irq.get().unwrap().set(true); + c.irq.get().unwrap().set(true); + assert_eq!(*crime.0.lock().unwrap(), vec![(IP32_SCSI_CRIME_INT, true), (IP32_SCSI_CRIME_INT_B, true)]); + } } diff --git a/src/pci.rs b/src/pci.rs index d96cf039e..26da57c83 100644 --- a/src/pci.rs +++ b/src/pci.rs @@ -9,7 +9,7 @@ use std::sync::{Arc, Mutex}; -use crate::irq::IrqLine; +use crate::irq::{InterruptController, IrqLine}; use crate::snapshot::{get_field, load_u32_slice, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_OK}; @@ -315,6 +315,67 @@ pub fn plug(dev: &dyn PciBusDevice, dma: Arc, irq_for_pin: impl FnOn } } +/// A host bridge's PCI interrupt pins and where they go. +/// +/// Each device number's INTA..INTD is a line ([`IntxTable::line`]). The +/// table keeps every pin's level and drives each system input it routes to +/// with the OR of the pins routed there, so on a shared input one function +/// cannot clear another's interrupt. A pin with no route is dropped. +/// +/// The routes are data, set when the bus is wired ([`IntxTable::route`]), +/// so wiring a function at another device number takes a call, not a new +/// bridge. +pub struct IntxTable { + /// Where the system inputs are. + intc: Arc, + st: Mutex, +} + +struct IntxState { + levels: Box<[bool]>, + input: Box<[Option]>, +} + +impl IntxTable { + /// A table for device numbers `0..devices`, every pin unrouted. + pub fn new(intc: Arc, devices: usize) -> Self { + let st = IntxState { levels: vec![false; devices * 4].into(), input: vec![None; devices * 4].into() }; + Self { intc, st: Mutex::new(st) } + } + + /// The line of `device`'s pin (1 = INTA). + pub fn line(device: u8, pin: u8) -> u32 { + device as u32 * 4 + (pin as u32 - 1) + } + + /// Wire `device`'s pin (1 = INTA) to system input `input`, or to none. + pub fn route(&self, device: u8, pin: u8, input: Option) { + self.st.lock().unwrap().input[Self::line(device, pin) as usize] = input; + } + + /// The system input `device`'s pin drives. + pub fn input(&self, device: u8, pin: u8) -> Option { + self.st.lock().unwrap().input.get(Self::line(device, pin) as usize).copied().flatten() + } + + /// Every pin's level, by line, for a snapshot. + pub fn levels(&self) -> Vec { + self.st.lock().unwrap().levels.to_vec() + } +} + +impl InterruptController for IntxTable { + fn set_line(&self, line: u32, level: bool) { + // The lock is held while the system input is driven, so two pins + // sharing an input cannot reach it out of order. + let mut st = self.st.lock().unwrap(); + let Some(&Some(input)) = st.input.get(line as usize) else { return }; + st.levels[line as usize] = level; + let any = st.levels.iter().zip(st.input.iter()).any(|(&l, &i)| l && i == Some(input)); + self.intc.set_line(input, any); + } +} + // ---- BARs on the physical bus ---- /// What a host bridge does to a CPU access on its way to a function: which @@ -608,7 +669,6 @@ mod tests { fn dma_write(&self, _: u64, _: &[u8]) -> PciDmaStatus { PciDmaStatus::MasterAbort } } - use crate::irq::InterruptController; struct Inputs(Mutex>); impl InterruptController for Inputs { fn set_line(&self, line: u32, level: bool) { self.0.lock().unwrap().push((line, level)); } @@ -634,4 +694,51 @@ mod tests { assert_eq!(*inputs.0.lock().unwrap(), vec![(43, true)]); } + #[test] + fn a_pin_drives_the_input_it_is_routed_to() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.route(3, 1, Some(10)); + assert_eq!(t.input(3, 1), Some(10)); + assert_eq!(t.input(3, 2), None); + t.set_line(IntxTable::line(3, 1), true); + t.set_line(IntxTable::line(3, 1), false); + assert_eq!(*inputs.0.lock().unwrap(), vec![(10, true), (10, false)]); + assert_eq!(IntxTable::line(3, 4), 15); + } + + /// A pin with no route, or beyond the table, is dropped: no input is + /// driven and its level is not kept. + #[test] + fn an_unrouted_pin_goes_nowhere() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.set_line(IntxTable::line(2, 1), true); + t.set_line(8 * 4, true); + assert!(inputs.0.lock().unwrap().is_empty()); + assert!(t.levels().iter().all(|&l| !l)); + } + + /// Two pins on one input: it stays up until both have dropped, and a + /// pin on another input does not hold it. + #[test] + fn a_shared_input_stays_up_until_every_pin_drops() { + let inputs = Arc::new(Inputs(Mutex::new(Vec::new()))); + let t = IntxTable::new(inputs.clone(), 8); + t.route(3, 1, Some(12)); + t.route(4, 2, Some(12)); + t.route(5, 1, Some(13)); + t.set_line(IntxTable::line(3, 1), true); + t.set_line(IntxTable::line(4, 2), true); + t.set_line(IntxTable::line(5, 1), true); + t.set_line(IntxTable::line(3, 1), false); + t.set_line(IntxTable::line(4, 2), false); + assert_eq!(*inputs.0.lock().unwrap(), + vec![(12, true), (12, true), (13, true), (12, true), (12, false)], + "input 12 drops only with its last pin"); + let mut levels = vec![false; 32]; + levels[IntxTable::line(5, 1) as usize] = true; + assert_eq!(t.levels(), levels, "slot 5's pin is still up"); + } + } From 711e1237d48eb6c55ae2c90dc0fde85358d2ce07 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:03:24 +0000 Subject: [PATCH 03/12] pci: let functions choose header write policies PCI functions need different writable and write-one-to-clear header fields. Describe those fields in one construction policy while keeping the current header defaults for existing functions. Forward narrow Bridge configuration writes with their byte enables so write-one-to-clear bits are handled correctly when a function supplies them. Existing Octane functions do not use that status policy, so their behavior stays unchanged. --- CHANGELOG.md | 7 ++ src/dev/bridge.rs | 2 +- src/pci.rs | 269 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 265 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73a77b8d..b10b95cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,6 +203,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 diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index 90d28122d..ac92c6a91 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -1055,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 } diff --git a/src/pci.rs b/src/pci.rs index 26da57c83..d17d8a194 100644 --- a/src/pci.rs +++ b/src/pci.rs @@ -46,6 +46,50 @@ pub enum Space { Mem, } +/// Initial value and writable bits for a function-specific configuration dword. +#[derive(Clone, Copy, Debug)] +pub struct PciRegisterPolicy { + /// Dword index. Header, BAR, subsystem identity, and interrupt dwords are managed separately. + pub reg_dword: usize, + /// Value restored by [`PciConfig::reset`]. + pub initial: u32, + /// Bits that configuration writes may change. + pub writable_mask: u32, +} + +/// Function-specific write behavior for a type 0 PCI configuration header. +/// +/// Register policies are copied into the header during construction; the +/// borrowed slice is not retained. +#[derive(Clone, Copy, Debug)] +pub struct PciConfigPolicy<'a> { + /// Fixed Status bits that read as one. + pub status: u16, + /// Device-latched Status bits that writes acknowledge with one. + pub status_w1c_mask: u16, + /// Writable Command bits. + pub command_mask: u16, + /// Writable bits in the latency timer byte. + pub latency_mask: u8, + /// Writable bits in the cache-line-size byte. + pub cache_line_mask: u8, + /// Initial values and write masks for function-specific dwords. + pub registers: &'a [PciRegisterPolicy], +} + +impl<'a> Default for PciConfigPolicy<'a> { + fn default() -> Self { + Self { + status: 0x0200, + status_w1c_mask: 0, + command_mask: 0xffff, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[], + } + } +} + /// One function's type 0 configuration header. /// /// Enough of it to be discovered and configured: identity, class, command @@ -61,6 +105,12 @@ pub struct PciConfig { /// Dwords `16..caps_end` hold a capability list, which firmware cannot /// overwrite. 0 when the function has none. caps_end: usize, + status: u16, + status_w1c_mask: u16, + command_mask: u16, + latency_mask: u8, + cache_line_mask: u8, + writable_masks: [u32; 64], } impl PciConfig { @@ -68,6 +118,27 @@ impl PciConfig { /// three bytes of register 0x08. `pin` is 0 for none, or [`PCI_INTA`] /// and on; the line register is left for firmware to assign. pub fn new(vendor: u16, device: u16, class: u32, revision: u8, bars: [Bar; 6], pin: u8) -> Self { + Self::new_with_policy( + vendor, + device, + class, + revision, + bars, + pin, + PciConfigPolicy::default(), + ) + } + + /// Construct a type 0 header with function-specific register behavior. + pub fn new_with_policy( + vendor: u16, + device: u16, + class: u32, + revision: u8, + bars: [Bar; 6], + pin: u8, + policy: PciConfigPolicy<'_>, + ) -> Self { let mut r = [0u32; 64]; r[0] = ((device as u32) << 16) | vendor as u32; r[2] = (class << 8) | revision as u32; @@ -77,7 +148,26 @@ impl PciConfig { r[4 + i] = 1; } } - Self { regs: Mutex::new(r), bars, power_on: r, caps_end: 0 } + let mut writable_masks = [u32::MAX; 64]; + for register in policy.registers { + assert!( + matches!(register.reg_dword, 10 | 12..=14 | 16..=63), + "pci: register policy requires a function-specific dword in 0..64"); + r[register.reg_dword] = register.initial; + writable_masks[register.reg_dword] = register.writable_mask; + } + Self { + regs: Mutex::new(r), + bars, + power_on: r, + status: policy.status, + status_w1c_mask: policy.status_w1c_mask, + command_mask: policy.command_mask, + latency_mask: policy.latency_mask, + cache_line_mask: policy.cache_line_mask, + writable_masks, + caps_end: 0, + } } /// Give the function a capability list: `caps` are the raw dwords of the @@ -113,24 +203,40 @@ impl PciConfig { pub fn read(&self, reg: usize) -> u32 { let r = self.regs.lock().unwrap(); match reg { - // Status: a medium-speed device with no errors to report. // A function with a capability list also sets status bit 4. - 1 => (r[1] & 0xffff) | 0x0200_0000 | if r[13] != 0 { 0x0010_0000 } else { 0 }, + 1 => (r[1] & (0xffff | ((self.status_w1c_mask as u32) << 16))) + | ((self.status as u32) << 16) + | if r[13] != 0 { 0x0010_0000 } else { 0 }, _ => r[reg], } } pub fn write(&self, reg: usize, val: u32) { + self.write_masked(reg, val, u32::MAX); + } + + /// Write selected bits of dword `reg`. Both `val` and `mask` use the + /// register's bit positions. Unselected Status events are not acknowledged. + pub fn write_masked(&self, reg: usize, val: u32, mask: u32) { + if mask == 0 { + return; + } let mut r = self.regs.lock().unwrap(); match reg { - // Identity and class are read-only. + // Identity, class and header type are read-only. 0 | 2 => {} - // Cache line size and latency timer are stored; the header type - // and BIST bytes above them are not. - 3 => r[3] = (r[3] & 0xffff_0000) | (val & 0xffff), - // Command; status bits are write-one-to-clear and none are set. - 1 => r[1] = val & 0xffff, + 3 => { + let mask = mask & (((self.latency_mask as u32) << 8) | self.cache_line_mask as u32); + r[3] = (r[3] & !mask) | (val & mask); + } + // Command writes replace selected bits; Status events clear on ones. + 1 => { + let clear = val & mask & ((self.status_w1c_mask as u32) << 16); + r[1] = (r[1] & !(mask & 0xffff) & !clear) + | (val & mask & self.command_mask as u32); + } 4..=9 => { + let val = (r[reg] & !mask) | (val & mask); let bar = self.bars[reg - 4]; r[reg] = match bar { Bar::Unused => 0, @@ -144,11 +250,27 @@ impl PciConfig { // The capability list is read-only. 16.. if reg < self.caps_end => {} // Interrupt pin is read-only; the line is firmware's scratch. - 15 => r[15] = (r[15] & 0xff00) | (val & !0xff00), - _ => r[reg] = val, + 15 => { + let mask = mask & !0xff00; + r[15] = (r[15] & !mask) | (val & mask); + } + _ => { + let mask = mask & self.writable_masks[reg]; + r[reg] = (r[reg] & !mask) | (val & mask); + } } } + /// Latch selected device Status events without replacing earlier events. + pub fn set_status_bits(&self, bits: u16) { + self.regs.lock().unwrap()[1] |= ((bits & self.status_w1c_mask) as u32) << 16; + } + + /// Set the board-assigned Subsystem ID, which survives function reset. + pub fn set_subsystem_identity(&self, value: u32) { + self.regs.lock().unwrap()[11] = value; + } + /// The base address firmware programmed into BAR `i`, type bits masked. pub fn bar_base(&self, i: usize) -> u32 { let v = self.regs.lock().unwrap()[4 + i]; @@ -512,6 +634,14 @@ mod tests { [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA) } + fn scsi_like_with_policy(policy: PciConfigPolicy<'_>) -> PciConfig { + PciConfig::new_with_policy( + 0x9004, 0x8078, 0x0100_00, 1, + [Bar::Io(0x100), Bar::Mem(0x1000), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], PCI_INTA, + policy, + ) + } + /// Firmware learns how much space a device wants by writing all ones to /// a BAR and reading back which address bits stuck. #[test] @@ -578,7 +708,7 @@ mod tests { #[test] fn reset_keeps_the_subsystem_identity() { let c = scsi_like(); - c.write(11, 0xc304_10a9); + c.set_subsystem_identity(0xc304_10a9); c.write(4, 0x0000_1000); c.reset(); assert_eq!(c.read(11), 0xc304_10a9); @@ -605,6 +735,121 @@ mod tests { assert_eq!(c.interrupt_pin(), PCI_INTA); } + #[test] + fn header_policies_mask_writes_and_restore_reset_values() { + for (status, command, latency, cache) in [(0x0200, 0xffff, 0, 0), (0x02b0, 0x0156, 0xf0, 0x0f)] { + let c = scsi_like_with_policy(PciConfigPolicy { + status, + status_w1c_mask: 0, + command_mask: command, + latency_mask: latency, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0020, + writable_mask: 0xff00, + }], + }); + c.write(1, u32::MAX); + c.write(3, u32::MAX); + c.write(18, u32::MAX); + assert_eq!(c.read(1), (u32::from(status) << 16) | u32::from(command)); + assert_eq!(c.read(3), (u32::from(latency) << 8) | u32::from(cache)); + assert_eq!(c.read(18), 0xaabb_ff20); + c.reset(); + assert_eq!(c.read(1), u32::from(status) << 16); + assert_eq!(c.read(3), 0); + assert_eq!(c.read(18), 0xaabb_0020); + } + } + + #[test] + fn masked_writes_preserve_unselected_fields_and_status_events() { + let c = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: 0x8040, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0xaabb_0000, + writable_mask: 0xffff, + }], + }); + c.set_status_bits(0x8040); + c.write(1, 0x0157); + c.write(3, 0x1234); + c.write(5, 0x1234_5000); + c.write(4, 0x1201); + for (reg, value, mask, expected) in [ + (1, 0xffff_0002, 0xff, 0x8240_0102), + (1, 0xffff_0000, 0x8000_0000, 0x0240_0102), + (1, 0x0040_0000, 0x00ff_0000, 0x0200_0102), + (1, u32::MAX, 0, 0x0200_0102), + (3, 0xffff_00ab, 0xff, 0x12ab), + (5, 0xabcd_ffff, 0xffff_0000, 0xabcd_5000), + (4, 0x3400, 0xff00, 0x3401), + (15, u32::MAX, 0xff00, 0x0100), + (18, u32::MAX, 0xff, 0xaabb_00ff), + ] { + c.write_masked(reg, value, mask); + assert_eq!(c.read(reg), expected, "register {reg}"); + } + c.set_status_bits(u16::MAX); + c.reset(); + assert_eq!(c.read(1), 0x0200_0000); + let default = scsi_like(); + default.set_status_bits(u16::MAX); + assert_eq!(default.read(1), 0x0200_0000); + } + + #[test] + fn snapshots_restore_registers_with_destination_write_policies() { + use crate::traits::Saveable; + let src = scsi_like_with_policy(PciConfigPolicy { + status: 0x02b0, + status_w1c_mask: 0xc000, + command_mask: 0x0157, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0, + writable_mask: 0x100, + }], + }); + src.set_status_bits(0x8000); + src.write(3, 0x1234); + src.write(18, 0x100); + let saved = src.save_state(); + for (events, command, cache) in [(0, 7, 0x0f), (0xc000, 0x0157, 0xff)] { + let dst = scsi_like_with_policy(PciConfigPolicy { + status: 0x0200, + status_w1c_mask: events, + command_mask: command, + latency_mask: 0, + cache_line_mask: cache, + registers: &[PciRegisterPolicy { + reg_dword: 18, + initial: 0x20, + writable_mask: 0x200, + }], + }); + dst.load_state(&saved).unwrap(); + assert_eq!(dst.save_state(), saved); + assert_eq!(dst.read(1), 0x0200_0000 | ((0x8000 & u32::from(events)) << 16)); + dst.write_masked(1, u32::MAX, u32::MAX); + dst.write(3, u32::MAX); + dst.write(18, u32::MAX); + assert_eq!(dst.read(1), 0x0200_0000 | u32::from(command)); + assert_eq!(dst.read(3), (0x1234 & !u32::from(cache)) | u32::from(cache)); + assert_eq!(dst.read(18), 0x300); + dst.reset(); + assert_eq!(dst.read(18), 0x20); + } + } + struct Nothing; impl BusDevice for Nothing {} From 1f6d1203bbf6071ce865fb9acbe38b59793ec4b9 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 04/12] isp1040: add the dual-channel ISP12160 part Fuel uses two independent SCSI buses behind one ISP12160 function. Reuse the ISP1040 command engine and let the part trait own the firmware, register and channel differences. A bus reset reaches every attached target as unit attention rather than a media change. Keep the reset attention on the shared target interface so a controller can notify each device through the same bus. The ISP12160 is its only caller: the WD33C93A, AIC-7880 and ISP1040 models do not call it, so their targets behave as before. A bus reset restores each disk's write cache bit per channel: a reset of one channel leaves the other's disks alone, and a chip or soft reset restores both. The engine changes reach the Octane's ISP1040: mailbox commands now run with dispatch held, and the target interface gains the bus reset. The Octane's IRIX 6.5.30 SCSI disk (Octane2, R10000, 256 MiB, V12, jitv2, release build, headless, NAT) boots to the login prompt and runs commands on this branch's tip and on main c6dea8c4: login after 126 s on the branch and 109 s on main in one run each (host load makes the times indicative only), the same `hinv` listing of both integral controllers and the disk, and the same command output. --- HACKING.md | 6 + src/dev/isp1040.rs | 395 ++++++++++++++++++++++++------- src/dev/isp1040/tests.rs | 488 ++++++++++++++++++++++++++++++++++++++- src/dev/isp12160.rs | 338 +++++++++++++++++++++++++++ src/dev/mod.rs | 1 + src/scsi.rs | 52 ++++- src/scsi/bus.rs | 11 +- 7 files changed, 1197 insertions(+), 94 deletions(-) create mode 100644 src/dev/isp12160.rs diff --git a/HACKING.md b/HACKING.md index e62f2ecae..3fbb9c9c6 100644 --- a/HACKING.md +++ b/HACKING.md @@ -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 diff --git a/src/dev/isp1040.rs b/src/dev/isp1040.rs index 1dc60951f..ae655f888 100644 --- a/src/dev/isp1040.rs +++ b/src/dev/isp1040.rs @@ -50,8 +50,9 @@ //! again when the hold ends: an approximation, a chip that had fetched it //! would plausibly move on. A parked entry the guest rewrites meanwhile is //! answered incomplete. A snapshot carries each bus's remaining hold and -//! the parked positions. The ISP12160 would widen `BUSES` and decode the -//! bus from mailbox 2 and its request entries; the per-bus state is there. +//! the parked positions. Only the ISP1040's bus reset starts a hold-off: +//! the ISP12160 (two buses, per-bus state is there) keeps its own bus-reset +//! handling, and its hold-off is not modelled. //! * Only logical unit 0 of a present target exists. A command for another //! unit is passed to the target with that unit number, which answers as //! SCSI says (INQUIRY: no device at this unit; anything else: check @@ -81,7 +82,7 @@ use parking_lot::{Condvar, Mutex, MutexGuard}; use crate::devlog::LogModule; use crate::irq::IrqLine; -use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PCI_INTA}; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciRegisterPolicy, PCI_INTA}; use crate::scsi::{scsi_cmd, ScsiBus, ScsiDataLength, ScsiIdentities, ScsiRequest, CDROM_IDENTITY, DISK_IDENTITY}; use crate::snapshot::{get_field, hex_u16, hex_u32, load_u16_slice, toml_u16, toml_u32, toml_u8, u16_slice_to_toml, u8_slice_to_toml}; @@ -115,7 +116,7 @@ pub mod reg { pub const WINDOW_SIZE: u32 = 0x100; } -const MAILBOX_COUNT: usize = 8; +pub(crate) const MAILBOX_COUNT: usize = 8; const MAILBOX_END: u32 = reg::MAILBOX_BASE + MAILBOX_COUNT as u32 * 2; const BANK_END: u32 = 0xc0; const BANK_WORDS: usize = ((BANK_END - reg::BANK_BASE) / 2) as usize; @@ -163,6 +164,8 @@ pub mod mbox { pub const INIT_REQ_QUEUE: u16 = 0x0010; pub const INIT_RSP_QUEUE: u16 = 0x0011; pub const ABORT_COMMAND: u16 = 0x0015; + pub const ABORT_DEVICE: u16 = 0x0016; + pub const ABORT_TARGET: u16 = 0x0017; pub const BUS_RESET: u16 = 0x0018; pub const GET_FIRMWARE_STATUS: u16 = 0x001f; pub const GET_TARGET_PARAMETERS: u16 = 0x0028; @@ -187,13 +190,14 @@ pub mod mbox { pub const INVALID: u16 = 0x4001; pub const HOST_INTERFACE_ERROR: u16 = 0x4002; pub const TEST_FAILED: u16 = 0x4003; + pub const COMMAND_ERROR: u16 = 0x4005; pub const PARAM_ERROR: u16 = 0x4006; } const RAM_WORDS: usize = 0x10000; /// The command budget the adapter announces in its firmware status. pub const MAX_REQUESTS: u16 = 512; -const FW_VERSION: [u16; 3] = [1, 0, 0]; +pub(crate) const FW_VERSION: [u16; 3] = [1, 0, 0]; pub const HOST_CMD_SHIFT: u32 = 12; pub const RISC_FLAG_HOST_INT: u16 = 0x0080; @@ -229,6 +233,7 @@ pub const SCSI_STATUS_SENSE_VALID: u16 = 0x0200; pub const IOCB_COMPLETE: u16 = 0x0000; pub const IOCB_INCOMPLETE: u16 = 0x0001; pub const IOCB_RESET: u16 = 0x0004; +pub const IOCB_ABORTED: u16 = 0x0005; pub const IOCB_DATA_UNDERRUN: u16 = 0x0015; pub const STATE_GOT_BUS: u16 = 0x0100; @@ -256,13 +261,14 @@ const DEFAULT_SELECTION_TIMEOUT: u16 = 250; /// source for a device, so it runs on the adapter's clock, host wall time /// less the time the machine was stopped, like the selection timeout. const BUS_RESET_DELAY_UNIT: Duration = Duration::from_secs(1); -/// The SCSI buses this part drives. The hold-off, the parked commands and the -/// selection are all kept per bus, so a two-bus part widens this and decodes -/// the bus from its own request entries and from mailbox 2. -const BUSES: usize = 1; +/// The most SCSI buses any part drives (the ISP12160's two; the ISP1040 has +/// one, `IspPart::channels`). The hold-off, the parked commands and the +/// selection are all kept per bus. Only the ISP1040's bus reset starts a +/// hold-off: the ISP12160 keeps its own bus-reset handling. +const BUSES: usize = 2; const CHECK_CONDITION: u8 = 0x02; -const MAX_TARGET: u8 = 15; +pub(crate) const MAX_TARGET: u8 = 15; const MAX_LUN: u8 = 7; /// A completion held for response-ring room. @@ -374,6 +380,49 @@ enum Prepared { Run(Job), } +/// Part-specific register and firmware behavior; the ring engine owns the state. +/// Defaults preserve the ISP1040 contract, including wrapping firmware RAM. +pub(crate) trait IspPart: Send + Sync { + fn name(&self) -> &'static str; + fn channels(&self) -> usize { 1 } + fn pci_config(&self, bars: [Bar; 6]) -> PciConfig { + PciConfig::new(PCI_VENDOR_QLOGIC, PCI_DEVICE_ISP1040, PCI_CLASS_SCSI, PCI_REVISION_ISP1040, bars, PCI_INTA) + } + fn firmware_version(&self) -> [u16; 3] { FW_VERSION } + fn active_bank(&self, cfg1: u16) -> Bank { + if cfg1 & CFG1_BANK_SELECT != 0 { Bank::Sxp } else { Bank::Risc } + } + fn checks_ram_bounds(&self) -> bool { false } + fn has_abort_commands(&self) -> bool { false } + fn target_on(&self, _channel: u8, target: u8) -> u8 { target } + fn decode_target(&self, target: u8) -> (usize, u8) { (0, target) } + fn id_high(&self) -> u16 { PCI_DEVICE_ISP1040 } + /// The ISP1040 register revision runs one ahead of its PCI revision. + fn bus_cfg0(&self) -> u16 { PCI_REVISION_ISP1040 as u16 + 1 } + fn cfg1_write_mask(&self) -> u16 { CFG1_WRITE_MASK } + fn unmodelled_dma_register(&self, _off: u32) -> bool { false } + fn completion_phases(&self, _moved: u64, _total: u64) -> u16 { STATE_ALL_PHASES & !STATE_GOT_SENSE } + fn mailbox_ready(&self, _semaphore: u16) -> bool { true } + /// Extra target-parameter results, beyond the common asynchronous/narrow reply. + fn clear_target_parameters(&self, _mailbox: &mut [u16; MAILBOX_COUNT]) {} + fn set_reset_delay(&self, st: &mut State) { st.reset_delay = st.mailbox[1]; } + /// Queue target notifications for the worker; the CPU never locks a bus. + fn reset_targets(&self, _st: &mut State) -> bool { false } + /// Handle extra mailbox commands, leaving result signalling to the engine. + fn execute_mailbox(&self, _core: &Core, _st: &mut MutexGuard<'_, State>) -> bool { false } + fn save_extra(&self, _st: &State, _t: &mut toml::map::Map) {} + fn load_extra(&self, _st: &mut State, _v: &toml::Value) {} +} + +struct Isp1040Part; + +impl IspPart for Isp1040Part { + fn name(&self) -> &'static str { "ISP1040" } +} + +/// The register window selected by BUS_CFG1, independent of its encoding. +pub(crate) enum Bank { Risc, Sxp, Sxp1, Dma } + /// A command waiting out its bus's post-reset hold-off. Only the ring /// position is kept: the entry, its data segments included, is read again /// from the guest's ring when the hold ends. @@ -392,7 +441,15 @@ struct Parked { started: bool, } -struct State { +pub(crate) struct State { + pub(crate) reset_targets: bool, + pub(crate) pending_target_resets: VecDeque<(u8, Option)>, + pub(crate) active: Option<(u8, u8, u32)>, + cancelled: bool, + pub(crate) outstanding: Vec<(u8, u8, u32)>, + pub(crate) sxp1_bank: [u16; BANK_WORDS], + pub(crate) dma_bank: [u16; BANK_WORDS], + pub(crate) reset_delay1: u16, ctl: Ctl, bus_cfg1: u16, bus_ctrl: u16, @@ -402,11 +459,11 @@ struct State { risc_flags: u16, dma_cmd_cfg: u16, dma_data_cfg: u16, - mailbox: [u16; MAILBOX_COUNT], + pub(crate) mailbox: [u16; MAILBOX_COUNT], risc_bank: [u16; BANK_WORDS], sxp_bank: [u16; BANK_WORDS], risc_ram: Vec, - reset_delay: u16, + pub(crate) reset_delay: u16, /// Mailbox command 0x0031's value, in [`SELECTION_TIMEOUT_UNIT`]s. selection_timeout: u16, rqst_base: u64, @@ -434,6 +491,14 @@ struct State { impl State { fn new() -> Self { let mut s = State { + reset_targets: false, + pending_target_resets: VecDeque::new(), + active: None, + cancelled: false, + outstanding: Vec::new(), + sxp1_bank: [0; BANK_WORDS], + dma_bank: [0; BANK_WORDS], + reset_delay1: 0, ctl: Ctl::default(), bus_cfg1: 0, bus_ctrl: 0, @@ -485,6 +550,12 @@ impl State { self.mailbox = [0, SIG1, SIG2, SIG3, SIG4, 0, 0, 0]; self.risc_bank = [0; BANK_WORDS]; self.sxp_bank = [0; BANK_WORDS]; + self.sxp1_bank = [0; BANK_WORDS]; + self.dma_bank = [0; BANK_WORDS]; + self.reset_delay1 = 0; + self.active = None; + self.cancelled = false; + self.outstanding.clear(); // The driver re-downloads firmware after every reset. self.risc_ram.iter_mut().for_each(|w| *w = 0); self.reset_delay = 0; @@ -499,6 +570,8 @@ impl State { self.resp_seen = 0; self.resp_cons = 0; self.pending.clear(); + self.reset_targets = false; + self.pending_target_resets.clear(); self.quiet_until = [None; BUSES]; self.parked.clear(); } @@ -524,9 +597,15 @@ impl State { self.resp_len != 0 && (u32::from(self.resp_prod) + 1) % u32::from(self.resp_len) != u32::from(self.resp_cons) } - fn active_bank(&mut self) -> &mut [u16; BANK_WORDS] { - if self.bus_cfg1 & CFG1_BANK_SELECT != 0 { &mut self.sxp_bank } else { &mut self.risc_bank } + fn active_bank(&mut self, bank: Bank) -> &mut [u16; BANK_WORDS] { + match bank { + Bank::Risc => &mut self.risc_bank, + Bank::Sxp => &mut self.sxp_bank, + Bank::Sxp1 => &mut self.sxp1_bank, + Bank::Dma => &mut self.dma_bank, + } } + } /// The adapter. Its state lives in a [`Core`] shared with the thread that @@ -543,8 +622,8 @@ impl std::ops::Deref for Isp1040 { /// Lock order: `line_order`, then `st`. Nothing else is taken with `st` /// held except the leaf locks (`notes`, `dma_misses`, `odd_accesses`): DMA, -/// the targets and `fence` are reached with it released. `fence` is held -/// across one guest-memory store at a time, and `targets` across one +/// either bus and `fence` are reached with it released. `fence` is held +/// across one guest-memory store at a time, and either bus across one /// target command; neither is held with the other. pub struct Core { st: Mutex, @@ -560,13 +639,17 @@ pub struct Core { wake: Condvar, /// Serialises driving the interrupt line; see [`Core::sync_irq`]. line_order: Mutex<()>, - /// The targets on the bus, by SCSI id. - bus: ScsiBus, - /// A bus reset the targets have not yet seen. `st` and `bus` are never - /// held together, so the reset (taken under `st`) is passed on by the - /// next command to take `bus`, before it runs: no command can see the - /// targets as they were. See [`Core::scsi`]. - targets_reset: AtomicBool, + /// Generation for commands cancelled by aborts without resetting the chip. + cancel: AtomicU64, + /// Immutable part policy, shared by register, worker and snapshot paths. + part: &'static dyn IspPart, + /// The controller's one bus, or both channels of the ISP12160. + buses: [ScsiBus; 2], + /// A bus reset the targets on that bus have not yet seen. `st` and the + /// buses are never held together, so the reset (taken under `st`) is + /// passed on by the next command to take the bus, before it runs: no + /// command can see the targets as they were. See [`Core::scsi`]. + targets_reset: [AtomicBool; 2], dma: OnceLock>, irq: OnceLock, /// Which of the Octane's two controllers this is (0 internal, 1 @@ -603,7 +686,11 @@ impl Isp1040 { /// The controller at `index` (0 internal, 1 external) on `bus`. pub fn new_at(index: u8, bus: ScsiBus) -> Self { - Self { core: Arc::new(Core::new_at(index, bus)), worker: Mutex::new(None) } + Self { core: Arc::new(Core::new_at(index, &Isp1040Part, [bus, Self::new_bus()])), worker: Mutex::new(None) } + } + + pub(crate) fn new_12160(bus0: ScsiBus, bus1: ScsiBus) -> Self { + Self { core: Arc::new(Core::new_at(0, &crate::dev::isp12160::Isp12160Part, [bus0, bus1])), worker: Mutex::new(None) } } /// Start the ring thread (idempotent) and let it take whatever was @@ -617,7 +704,7 @@ impl Isp1040 { let c = self.core.clone(); *worker = Some( std::thread::Builder::new() - .name(format!("ISP1040-{}", self.index)) + .name(format!("{}-{}", self.core.model_name(), self.index)) .spawn(move || c.run_ring()) .expect("spawn isp1040 thread"), ); @@ -641,13 +728,17 @@ impl Isp1040 { /// Name the adapter's long-lived locks in the lock registry, which /// holds only a weak reference to them. pub fn register_locks(&self) { - for (what, probe) in [ + let mut locks: Vec<(&str, fn(&Core) -> bool)> = vec![ ("st", (|c: &Core| c.st.is_locked()) as fn(&Core) -> bool), ("fence", |c| c.fence.is_locked()), - ("targets", |c| c.bus.is_locked()), - ] { + ("targets", |c| c.buses[0].is_locked()), + ]; + if self.core.part.channels() > 1 { + locks.push(("targets1", |c| c.buses[1].is_locked())); + } + for (what, probe) in locks { let core = Arc::downgrade(&self.core); - crate::locks::register_lock(format!("isp1040-{}::{what}", self.index), move || { + crate::locks::register_lock(format!("{}-{}::{what}", self.core.model_name().to_ascii_lowercase(), self.index), move || { core.upgrade().is_some_and(|c| probe(&c)) }); } @@ -662,27 +753,27 @@ impl Drop for Isp1040 { } impl Core { - fn new_at(index: u8, bus: ScsiBus) -> Self { + fn new_at(index: u8, part: &'static dyn IspPart, buses: [ScsiBus; 2]) -> Self { let st = State::new(); let gen = st.ctl.gen; + let bars = [Bar::Io(reg::WINDOW_SIZE), Bar::Mem(reg::WINDOW_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused]; + let pci = part.pci_config(bars); Self { + part, + cancel: AtomicU64::new(0), st: Mutex::new(st), gen: AtomicU64::new(gen), fence: Mutex::new(()), wake: Condvar::new(), line_order: Mutex::new(()), - bus, - targets_reset: AtomicBool::new(false), + buses, + targets_reset: [AtomicBool::new(false), AtomicBool::new(false)], dma: OnceLock::new(), irq: OnceLock::new(), index, // BAR 0 is the I/O window and BAR 1 its memory-space alias, one // register file. - pci: PciConfig::new( - PCI_VENDOR_QLOGIC, PCI_DEVICE_ISP1040, PCI_CLASS_SCSI, PCI_REVISION_ISP1040, - [Bar::Io(reg::WINDOW_SIZE), Bar::Mem(reg::WINDOW_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], - PCI_INTA, - ), + pci, notes: Mutex::new(Vec::new()), dma_misses: Mutex::new(std::collections::BTreeSet::new()), odd_accesses: Mutex::new(0), @@ -692,6 +783,8 @@ impl Core { } } + fn model_name(&self) -> &'static str { self.part.name() } + pub fn index(&self) -> u8 { self.index } /// A machine reset: the chip as at power-on (no firmware, no queues), @@ -702,6 +795,7 @@ impl Core { { let mut st = self.st.lock(); st.reset(); + if self.part.reset_targets(&mut st) { self.kick(&mut st); } self.publish_gen(&st); self.wake.notify_all(); } @@ -929,7 +1023,7 @@ impl Core { } /// Tell the ring thread the ring may have moved. - fn kick(&self, st: &mut State) { + pub(crate) fn kick(&self, st: &mut State) { st.ctl.work = true; self.wake.notify_all(); } @@ -957,11 +1051,16 @@ impl Core { /// The SCSI bus: attach targets to it and ask what is on it. pub fn bus(&self) -> &ScsiBus { - &self.bus + &self.buses[0] + } + + pub(crate) fn bus_for_channel(&self, channel: u8) -> Option<&ScsiBus> { + ((channel as usize) < self.part.channels()).then(|| &self.buses[channel as usize]) } - fn has_target(&self, id: u8) -> bool { - self.bus.has_target(id) + fn bus_for_target(&self, target: u8) -> (&ScsiBus, u8) { + let (channel, id) = self.part.decode_target(target); + (&self.buses[channel], id) } // ---- DMA --------------------------------------------------------- @@ -1045,12 +1144,18 @@ impl Core { /// A RISC RAM load or dump of up to 128 KiB, with the state unlocked /// for the DMA. The whole transfer is read before a word of it is kept, /// so a refused DMA leaves the array as it was. Words travel - /// little-endian. This part has no edge to its array: a transfer past - /// the end folds to the start. - fn ram_transfer(&self, st: &mut MutexGuard<'_, State>, load: bool) -> u16 { + /// little-endian. The ISP1040 wraps past the end; the ISP12160 rejects + /// transfers that would cross it. + pub(crate) fn ram_transfer(&self, st: &mut MutexGuard<'_, State>, load: bool) -> u16 { let base = st.mailbox[1]; let count = st.mailbox[4] as usize; - let host = Self::mbox_host_addr(&st.mailbox); + if self.part.checks_ram_bounds() && base as usize + count > RAM_WORDS { + return mbox::PARAM_ERROR; + } + let mut host = Self::mbox_host_addr(&st.mailbox); + if st.mailbox[0] == mbox::LOAD_RAM_A64 { + host |= ((st.mailbox[7] as u64) << 32) | ((st.mailbox[6] as u64) << 48); + } if count == 0 { return mbox::COMPLETE; } @@ -1077,6 +1182,20 @@ impl Core { /// Synchronous: the protocol has no way to leave a command pending. fn mailbox_command(&self, st: &mut MutexGuard<'_, State>) { let cmd = st.mailbox[0]; + if self.part.has_abort_commands() && matches!(cmd, mbox::ABORT_COMMAND | mbox::ABORT_DEVICE | mbox::ABORT_TARGET | mbox::BUS_RESET) { + self.with_dispatch_held(st, |st| self.execute_mailbox_command(st)); + } else { + self.execute_mailbox_command(st); + } + } + + fn execute_mailbox_command(&self, st: &mut MutexGuard<'_, State>) { + let cmd = st.mailbox[0]; + if self.part.execute_mailbox(self, st) { + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } match cmd { mbox::NOP => st.mailbox[0] = mbox::COMPLETE, mbox::WRITE_RAM_WORD => { @@ -1098,7 +1217,19 @@ impl Core { // zero. mbox::VERIFY_CHECKSUM => { let start = st.mailbox[1]; + if self.part.checks_ram_bounds() && start as usize + 4 > RAM_WORDS { + st.mailbox[0] = mbox::PARAM_ERROR; + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } let len = st.risc_ram[start.wrapping_add(3) as usize]; + if self.part.checks_ram_bounds() && start as usize + len as usize > RAM_WORDS { + st.mailbox[0] = mbox::PARAM_ERROR; + st.semaphore = SEMAPHORE_LOCK; + st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; + return; + } let mut sum = 0u16; for i in 0..len { sum = sum.wrapping_add(st.risc_ram[start.wrapping_add(i) as usize]); @@ -1111,9 +1242,7 @@ impl Core { // Accepted, never executed. mbox::EXEC_FIRMWARE => st.mailbox[0] = mbox::COMPLETE, mbox::ABOUT_FIRMWARE => { - st.mailbox[1] = FW_VERSION[0]; - st.mailbox[2] = FW_VERSION[1]; - st.mailbox[3] = FW_VERSION[2]; + st.mailbox[1..4].copy_from_slice(&self.part.firmware_version()); st.mailbox[6] = 0; st.mailbox[0] = mbox::COMPLETE; } @@ -1128,6 +1257,7 @@ impl Core { mbox::GET_TARGET_PARAMETERS => { st.mailbox[2] = 0; st.mailbox[3] = 0; + self.part.clear_target_parameters(&mut st.mailbox); st.mailbox[0] = mbox::COMPLETE; } // Setters for state nothing reads back; answering invalid is a @@ -1149,7 +1279,7 @@ impl Core { st.mailbox[0] = mbox::COMPLETE; } mbox::SET_RESET_DELAY_PARAMS => { - st.reset_delay = st.mailbox[1]; + self.part.set_reset_delay(st); st.mailbox[0] = mbox::COMPLETE; } mbox::INIT_REQ_QUEUE | mbox::INIT_REQ_QUEUE_A64 | mbox::INIT_RSP_QUEUE | mbox::INIT_RSP_QUEUE_A64 => { @@ -1204,7 +1334,7 @@ impl Core { mbox::BUS_RESET => { let now = self.now_ns(st); let bus = usize::from(st.mailbox[2]); - if bus >= BUSES { + if bus >= self.part.channels() { // A bus this part does not have: nothing to quiet or // retire, and the datasheet's per-bus hold leaves the // real bus alone. Noted so a driver that leaves mailbox 2 @@ -1228,7 +1358,7 @@ impl Core { st.pending.push_back(Pending { handle: p.handle, completion: IOCB_RESET, ..Default::default() }); } } - self.targets_reset.store(true, Ordering::Release); + self.targets_reset[bus].store(true, Ordering::Release); let delay = BUS_RESET_DELAY_UNIT * u32::from(st.mailbox[1]); st.quiet_until[bus] = (!delay.is_zero()).then(|| now.saturating_add(delay.as_nanos() as i64)); self.kick(st); @@ -1245,6 +1375,67 @@ impl Core { st.bus_status |= STATUS_INT_REQ | STATUS_RISC_INT_REQ; } + /// Prevent the worker from taking another entry while a mailbox drops `st`. + fn with_dispatch_held(&self, st: &mut MutexGuard<'_, State>, f: impl FnOnce(&mut MutexGuard<'_, State>) -> R) -> R { + struct Hold<'a, 'b> { + core: &'a Core, + st: &'a mut MutexGuard<'b, State>, + } + impl Drop for Hold<'_, '_> { + fn drop(&mut self) { + self.st.ctl.hold -= 1; + self.core.kick(self.st); + } + } + st.ctl.hold += 1; + let held = Hold { core: self, st }; + f(held.st) + } + + /// A reset of `channel`'s bus: each disk on it puts its mode parameters back before the next + /// command, see [`ScsiTarget::reset_mode_parameters`]. A channel the part lacks is ignored. + pub(crate) fn note_bus_reset(&self, channel: usize) { + if let Some(flag) = self.targets_reset.get(channel) { flag.store(true, Ordering::Release); } + } + + /// Notify targets with `st` released; a blocked target must not stall register access. + fn reset_targets(&self, st: &mut MutexGuard<'_, State>, channel: u8, target: Option) { + let Some(bus) = self.bus_for_channel(channel) else { return }; + self.with_dispatch_held(st, |st| MutexGuard::unlocked(st, || bus.for_each(|id, dev| { + if target.is_none_or(|target| target as usize == id) { + dev.bus_reset(); + } + }))); + } + + pub(crate) fn retire_outstanding(&self, st: &mut State, matches: impl Fn((u8, u8, u32)) -> bool, completion: u16) -> usize { + let mut retired = 0; + let mut keep = Vec::new(); + for item in st.outstanding.drain(..) { + if matches(item) { + st.pending.push_back(Pending { handle: item.2, completion, ..Pending::default() }); + retired += 1; + } else { + keep.push(item); + } + } + st.outstanding = keep; + if retired != 0 { self.kick(st); } + retired + } + + /// Queue one cancellation reply, then fence its remaining guest-memory stores. + pub(crate) fn cancel_active(&self, st: &mut MutexGuard<'_, State>, completion: u16) { + if st.cancelled { return; } + if let Some((_, _, handle)) = st.active { + self.cancel.fetch_add(1, Ordering::AcqRel); + st.cancelled = true; + st.pending.push_back(Pending { handle, completion, ..Pending::default() }); + self.kick(st); + MutexGuard::unlocked(st, || self.retire_stores()); + } + } + fn host_command(&self, st: &mut MutexGuard<'_, State>, val: u16) { match val >> HOST_CMD_SHIFT { 0x0 => {} @@ -1255,6 +1446,10 @@ impl Core { 0x3 => st.risc_flags &= !(RISC_FLAG_PAUSED | RISC_FLAG_RESET), // "A mailbox command is ready": the trigger to process one. 0x5 => { + if !self.part.mailbox_ready(st.semaphore) { + self.note("mailbox submitted with an unacknowledged result".into()); + return; + } st.risc_flags |= RISC_FLAG_HOST_INT; self.mailbox_command(st); } @@ -1440,17 +1635,19 @@ impl Core { /// Run a CDB against the target; `data` is what a write sends it. None /// when a reset cancelled the command while it waited for the target. - fn scsi(&self, gen: u64, target: u8, cdb: &[u8], data: Option>) -> Option<(Vec, u8)> { - let mut targets = self.bus.lock(); - if self.targets_reset.swap(false, Ordering::AcqRel) { + fn scsi(&self, gen: u64, cancel: u64, target: u8, cdb: &[u8], data: Option>) -> Option<(Vec, u8)> { + let (channel, _) = self.part.decode_target(target); + let (bus, id) = self.bus_for_target(target); + let mut targets = bus.lock(); + if self.targets_reset[channel].swap(false, Ordering::AcqRel) { // The bus reset puts each target's mode parameters back; see // [`ScsiTarget::reset_mode_parameters`]. targets.iter_mut().flatten().for_each(|t| t.reset_mode_parameters()); } - if self.gen.load(Ordering::Acquire) != gen { + if self.gen.load(Ordering::Acquire) != gen || self.cancel.load(Ordering::Acquire) != cancel { return None; } - let Some(dev) = targets.get_mut(target as usize).and_then(|t| t.as_mut()) else { + let Some(dev) = targets.get_mut(id as usize).and_then(|t| t.as_mut()) else { return Some((Vec::new(), CHECK_CONDITION)); }; let req = ScsiRequest { cdb: cdb.to_vec(), data_len: ScsiDataLength::Unlimited, data_in: data }; @@ -1481,14 +1678,15 @@ impl Core { let lun = (device & 0xff) as u8; // A bus this part does not have. - if usize::from(channel) >= BUSES { + if channel as usize >= self.part.channels() { return Prepared::Answered(Self::incomplete(handle, 0, 0)); } // GOT_BUS alone is Linux's bad-target; the timeout bit beside it is // what lets the empty slot be treated as benign. - if target > MAX_TARGET || !self.has_target(target) { + if target > MAX_TARGET || !self.buses[channel as usize].has_target(target) { return Prepared::Absent(channel as u8, Self::incomplete(handle, STATE_GOT_BUS, STATUS_FLAG_TIMEOUT)); } + let id = target; if lun > MAX_LUN { return Prepared::Answered(Self::incomplete(handle, STATE_GOT_BUS, STATUS_FLAG_TIMEOUT)); } @@ -1497,9 +1695,9 @@ impl Core { cdb[1] = (cdb[1] & 0x1f) | (lun << 5); let cdb = &cdb[..(cdb_len as usize).clamp(1, CDB_FIELD_LEN)]; let block = self - .bus + .buses[channel as usize] .lock() - .get(target as usize) + .get(id as usize) .and_then(|t| t.as_ref()) .and_then(|t| t.block_size()) .map(|b| b as usize) @@ -1507,6 +1705,7 @@ impl Core { .unwrap_or(BLOCK_SIZE); let (xfer, to_dev) = Self::cdb_transfer(cdb, block); let (segs, described) = self.read_segments(ring, addr, entry_count, seg_count, xfer as u64, a64); + let target = self.part.target_on(channel as u8, target); Prepared::Run(Job { handle, target, cdb: cdb.to_vec(), xfer, to_dev, segs, described }) } @@ -1522,7 +1721,7 @@ impl Core { /// None too when a reset cancelled the command (`gen` is the generation /// it was read under): what it had not yet stored in host memory is not /// stored, and no completion is built. - fn run_job(&self, job: Job, gen: u64) -> Option { + fn run_job(&self, job: Job, gen: u64, cancel: u64) -> Option { let Job { handle, target, cdb, xfer, to_dev, segs, described } = job; // The bar has lights for SCSI ids 0-5. if let (Some(hb), 0..=5) = (self.heartbeat.get(), target) { @@ -1540,9 +1739,10 @@ impl Core { buf.extend_from_slice(&chunk); } moved = buf.len() as u64; - status = self.scsi(gen, target, &cdb, Some(buf))?.1; + if self.cancel.load(Ordering::Acquire) != cancel { return None; } + status = self.scsi(gen, cancel, target, &cdb, Some(buf))?.1; } else { - let (data, s) = self.scsi(gen, target, &cdb, None)?; + let (data, s) = self.scsi(gen, cancel, target, &cdb, None)?; status = s; // Offer the target's bytes to the descriptors in order. let take = (data.len() as u64).min(described).min(xfer as u64) as usize; @@ -1550,7 +1750,7 @@ impl Core { for &(a, l) in &segs { if off >= take { break; } let n = (l as usize).min(take - off); - if !self.fenced(gen, || self.dma_write(a, &data[off..off + n]))? { + if !self.fenced(gen, || self.cancel.load(Ordering::Acquire) == cancel && self.dma_write(a, &data[off..off + n]))? { self.note(format!("payload DMA for handle {handle:#x} refused by the fabric; command stopped")); return None; } @@ -1561,12 +1761,13 @@ impl Core { // Auto-sense runs only after a CHECK CONDITION. let mut sense = Vec::new(); + if self.cancel.load(Ordering::Acquire) != cancel { return None; } if status == CHECK_CONDITION { - sense = self.scsi(gen, target, &[scsi_cmd::REQUEST_SENSE, 0, 0, 0, SENSE_MAX as u8, 0], None)?.0; + sense = self.scsi(gen, cancel, target, &[scsi_cmd::REQUEST_SENSE, 0, 0, 0, SENSE_MAX as u8, 0], None)?.0; sense.truncate(SENSE_MAX); } let mut scsi_status = status as u16; - let mut state_flags = STATE_ALL_PHASES & !STATE_GOT_SENSE; + let mut state_flags = self.part.completion_phases(moved, described.max(xfer as u64)); if !sense.is_empty() { scsi_status |= SCSI_STATUS_SENSE_VALID; state_flags |= STATE_GOT_SENSE; @@ -1663,6 +1864,17 @@ impl Core { st.ctl.work = true; return st; } + if std::mem::take(&mut st.reset_targets) { + for channel in 0..self.part.channels() { + self.note_bus_reset(channel); + self.reset_targets(&mut st, channel as u8, None); + } + continue; + } + if let Some((channel, target)) = st.pending_target_resets.pop_front() { + self.reset_targets(&mut st, channel, target); + continue; + } // Completions held for room go first, in order, as the driver // frees it; while any is held the request ring waits. if !st.pending.is_empty() { @@ -1804,8 +2016,16 @@ impl Core { } Prepared::Run(job) => { st.total_cmds += 1; + st.active = Some((job.target, job.cdb.get(1).copied().unwrap_or(0) >> 5, job.handle)); + st.cancelled = false; st.ctl.busy = true; - let done = MutexGuard::unlocked(&mut st, || self.run_job(job, ring.gen)); + let cancel = self.cancel.load(Ordering::Acquire); + let mut done = MutexGuard::unlocked(&mut st, || self.run_job(job, ring.gen, cancel)); + if st.cancelled { done = None; } + else if self.part.has_abort_commands() && done.is_none() && st.ctl.gen == ring.gen { + if let Some(active) = st.active { st.outstanding.push(active); } + } + st.active = None; st.ctl.busy = false; self.wake.notify_all(); // A reset cancelled the command. @@ -1838,13 +2058,16 @@ impl Core { return st.mailbox[((off - reg::MAILBOX_BASE) / 2) as usize]; } if (reg::BANK_BASE..BANK_END).contains(&off) { - return st.active_bank()[((off - reg::BANK_BASE) / 2) as usize]; + let bank = self.part.active_bank(st.bus_cfg1); + return st.active_bank(bank)[((off - reg::BANK_BASE) / 2) as usize]; + } + if self.part.unmodelled_dma_register(off) { + return 0; } match off { reg::ID_LOW => PCI_VENDOR_QLOGIC, - reg::ID_HIGH => PCI_DEVICE_ISP1040, - // The revision field runs one ahead of the PCI byte. - reg::BUS_CFG0 => PCI_REVISION_ISP1040 as u16 + 1, + reg::ID_HIGH => self.part.id_high(), + reg::BUS_CFG0 => self.part.bus_cfg0(), reg::BUS_CFG1 => st.bus_cfg1, reg::BUS_CTRL => st.bus_ctrl, reg::BUS_STATUS => st.bus_status, @@ -1862,6 +2085,10 @@ impl Core { } fn reg_write(&self, off: u32, val: u16) { + if self.part.unmodelled_dma_register(off) { + self.note(format!("write to ISP12160 DMA register {off:#04x} is not modelled")); + return; + } let mut st = self.st.lock(); if off == reg::RESPONSE_QUEUE { // The consumer acknowledgement: recorded, never over the @@ -1884,15 +2111,17 @@ impl Core { return; } if (reg::BANK_BASE..BANK_END).contains(&off) { - st.active_bank()[((off - reg::BANK_BASE) / 2) as usize] = val; + let bank = self.part.active_bank(st.bus_cfg1); + st.active_bank(bank)[((off - reg::BANK_BASE) / 2) as usize] = val; return; } match off { reg::BUS_CFG1 => { - if val & !CFG1_WRITE_MASK != 0 { - self.note(format!("bus configuration 1 bits {:#06x} are not modelled", val & !CFG1_WRITE_MASK)); + let mask = self.part.cfg1_write_mask(); + if val & !mask != 0 { + self.note(format!("bus configuration 1 bits {:#06x} are not modelled", val & !mask)); } - st.bus_cfg1 = val & CFG1_WRITE_MASK; + st.bus_cfg1 = val & mask; } // Applied after the enables are stored, so the reset wins. reg::BUS_CTRL => { @@ -1900,6 +2129,7 @@ impl Core { // The command in flight is cancelled, not waited for. if val & CTRL_SOFT_RESET != 0 { st.reset(); + if self.part.reset_targets(&mut st) { self.kick(&mut st); } self.publish_gen(&st); self.wake.notify_all(); drop(st); @@ -2005,7 +2235,7 @@ impl crate::traits::Device for Isp1040 { impl crate::scsi_monitor::ScsiTargets for Isp1040 { fn scsi_bus(&self) -> &ScsiBus { - &self.bus + &self.buses[0] } } @@ -2041,14 +2271,15 @@ impl crate::traits::Saveable for Isp1040 { // A hold-off already over but not acted on still crosses: the // commands parked behind it start when the load re-arms it at 0. let quiet_left = st.quiet_until.map(|until| until.map(|u| u - now)); - Self::save_locked(st, left, quiet_left) + self.save_locked(st, left, quiet_left) }) } fn load_state(&self, v: &toml::Value) -> Result<(), String> { self.quiesced(|st| { let now = self.now_ns(st); - Self::load_locked(st, v, now); + self.load_locked(st, v, now); + self.cancel.store(0, Ordering::Release); self.publish_gen(st); }); self.sync_irq(); @@ -2057,7 +2288,7 @@ impl crate::traits::Saveable for Isp1040 { } impl Isp1040 { - fn save_locked(st: &State, selecting_left: Option, quiet_left: [Option; BUSES]) -> toml::Value { + fn save_locked(&self, st: &State, selecting_left: Option, quiet_left: [Option; BUSES]) -> toml::Value { let mut t = toml::map::Map::new(); for (k, v) in [ ("bus_cfg1", st.bus_cfg1), ("bus_ctrl", st.bus_ctrl), ("bus_status", st.bus_status), @@ -2130,16 +2361,18 @@ impl Isp1040 { m.insert("sense".into(), u8_slice_to_toml(&sel.done.sense)); t.insert("selecting".into(), toml::Value::Table(m)); } + self.part.save_extra(st, &mut t); toml::Value::Table(t) } /// Unprocessed request entries survive: the ring thread is kicked and /// runs them after the load. - fn load_locked(st: &mut State, v: &toml::Value, now: i64) { + fn load_locked(&self, st: &mut State, v: &toml::Value, now: i64) { { let ctl = std::mem::take(&mut st.ctl); *st = State::new(); st.ctl = ctl; + self.part.load_extra(st, v); st.ctl.gen += 1; st.ctl.rqst_epoch += 1; st.ctl.resp_epoch += 1; diff --git a/src/dev/isp1040/tests.rs b/src/dev/isp1040/tests.rs index 8a96b9d66..0bb7e084c 100644 --- a/src/dev/isp1040/tests.rs +++ b/src/dev/isp1040/tests.rs @@ -2,7 +2,8 @@ //! and ring contract. Host memory is a flat byte array behind a fake `PciDma`; the disk //! is a small raw image. //! -//! Ported from tests/qtest/qlogic-isp-test.c in jamesbraid/qemu-sgi-origami. +//! Ported from tests/qtest/qlogic-isp-test.c and tests/qtest/sn0-isp12160-test.c +//! in jamesbraid/qemu-sgi-origami. use super::*; use crate::irq::LevelProbe; @@ -59,15 +60,16 @@ fn image(name: &str, blocks: usize) -> std::path::PathBuf { } impl Rig { - /// A chip on fake memory, no disks. - fn bare() -> Rig { + fn on(isp: Isp1040) -> Rig { let mem = FakeMem::new(); - let isp = Isp1040::new(); Device::start(&isp); isp.connect_dma(mem.clone()); Rig { isp, mem } } + /// A chip on fake memory, no disks. + fn bare() -> Rig { Rig::on(Isp1040::new()) } + /// A chip with a disk of `blocks` blocks at target `id`. fn with_disk(name: &str, id: u8, blocks: usize) -> Rig { let r = Rig::bare(); @@ -1616,6 +1618,403 @@ fn a_controller_built_on_a_filled_bus_answers_for_its_target() { let _ = std::fs::remove_file(img); } + +struct MarkerTarget(u8); + +impl crate::scsi::ScsiTarget for MarkerTarget { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: vec![self.0] }) + } +} + +/// Target 0 on channel 1 must stay distinct from target 0 on channel 0. +#[test] +fn the_isp12160_routes_ring_requests_to_separate_buses() { + let bus0 = Isp1040::new_bus(); + let bus1 = Isp1040::new_bus(); + bus0.attach(0, Box::new(MarkerTarget(0x10))).unwrap(); + bus1.attach(0, Box::new(MarkerTarget(0x20))).unwrap(); + let r = Rig::on(Isp1040::new_12160(bus0, bus1)); + r.init_rings(4, 4); + + r.put_entry(0, &request(0x1234, 0x80, &[0x12, 0, 0, 0, 36, 0], &[(BUF, 36)])); + r.doorbell(1); + assert_eq!(r.mem.get(BUF, 1), vec![0x20], "channel 1 reaches its target 0"); + + r.put_entry(1, &request(0x1235, 0, &[0x12, 0, 0, 0, 36, 0], &[(BUF + 64, 36)])); + r.doorbell(2); + assert_eq!(r.mem.get(BUF + 64, 1), vec![0x10], "channel 0 keeps its own target 0"); +} + +struct ResetProbe(Arc); + +impl crate::scsi::ScsiTarget for ResetProbe { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: Vec::new() }) + } + + fn bus_reset(&mut self) { + self.0.fetch_add(1, Ordering::Relaxed); + } +} + +/// A snapshot retains both the chip-wide reset and the selected target reset, +/// and the resumed worker delivers both through the original channel buses. +#[test] +fn the_isp12160_preserves_queued_bus_and_target_resets_across_save_load() { + let src = Isp1040::new_12160(Isp1040::new_bus(), Isp1040::new_bus()); + { + let mut state = src.st.lock(); + state.reset_targets = true; + state.pending_target_resets.push_back((1, Some(2))); + } + let saved = src.save_state(); + + let resets0 = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let resets1 = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let selected_reset = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let bus0 = Isp1040::new_bus(); + let bus1 = Isp1040::new_bus(); + bus0.attach(1, Box::new(ResetProbe(resets0.clone()))).unwrap(); + bus1.attach(1, Box::new(ResetProbe(resets1.clone()))).unwrap(); + bus1.attach(2, Box::new(ResetProbe(selected_reset.clone()))).unwrap(); + let dst = Isp1040::new_12160(bus0, bus1); + dst.load_state(&saved).unwrap(); + Device::start(&dst); + dst.wait_idle(); + Device::stop(&dst); + + assert_eq!(resets0.load(Ordering::Relaxed), 1); + assert_eq!(resets1.load(Ordering::Relaxed), 1); + assert_eq!(selected_reset.load(Ordering::Relaxed), 2, "the full reset and queued target reset both run"); + assert_eq!(saved.get("reset_targets").and_then(toml::Value::as_bool), Some(true)); + assert_eq!(saved.get("target_resets").and_then(toml::Value::as_array).map(Vec::len), Some(1)); + let after = dst.save_state(); + assert_eq!(after.get("reset_targets").and_then(toml::Value::as_bool), Some(false)); + assert_eq!(after.get("target_resets").and_then(toml::Value::as_array).map(Vec::len), Some(0)); +} + +// ---- ISP12160 second channel and recovery commands ----------------------- + +fn rig12160() -> Rig { Rig::on(Isp1040::new_12160(Isp1040::new_bus(), Isp1040::new_bus())) } + +/// A 64-block image whose first block is `fill`, so a read says which disk answered. +fn filled_image(name: &str, fill: u8) -> std::path::PathBuf { + let p = image(name, 64); + let mut data = vec![0u8; 64 * BLOCK_SIZE]; + data[..BLOCK_SIZE].fill(fill); + std::fs::write(&p, data).expect("test image"); + p +} + +/// A 12160 with a different disk at target 1 on each channel. +fn disks_on_both_channels(name: &str) -> Rig { + let r = rig12160(); + for channel in 0..2u8 { + let img = filled_image(&format!("{name}{channel}"), 0xa0 + channel); + r.isp.bus_for_channel(channel).unwrap().attach_image(1, &img, false).expect("attach"); + } + r +} + +/// Issues entries one at a time on a 16-entry ring and returns each completion. +struct Seq<'a> { + r: &'a Rig, + n: u16, +} + +impl<'a> Seq<'a> { + fn new(r: &'a Rig) -> Self { + r.init_rings(16, 16); + Seq { r, n: 0 } + } + fn run(&mut self, handle: u32, target: u8, cdb: &[u8], segs: &[(u32, u32)]) -> [u8; ENTRY_SIZE] { + let slot = self.n; + self.r.put_entry(slot as u32, &request(handle, target, cdb, segs)); + self.n += 1; + self.r.doorbell(self.n); + self.r.status(slot as u32) + } +} + +const TUR: [u8; 6] = [0; 6]; + +/// Counts bus resets and answers any command with a full block pair, so a read +/// whose second buffer segment is unreachable stays outstanding. +struct CountingDisk(Arc); + +impl crate::scsi::ScsiTarget for CountingDisk { + fn request(&mut self, _req: &crate::scsi::ScsiRequest) -> std::io::Result { + Ok(crate::scsi::ScsiResponse { status: 0, data: vec![0; 2 * BLOCK_SIZE] }) + } + fn bus_reset(&mut self) { self.0.fetch_add(1, Ordering::Relaxed); } +} + +struct Held { + r: Rig, + /// Bus resets seen by the counting disks at target 1 on channel 0, target 1 on channel 1, + /// and target 2 on channel 1. + ch0: Arc, + ch1: Arc, + ch1_other: Arc, +} + +impl Held { + fn new() -> Held { + let counters: Vec<_> = (0..3).map(|_| Arc::new(std::sync::atomic::AtomicUsize::new(0))).collect(); + let (bus0, bus1) = (Isp1040::new_bus(), Isp1040::new_bus()); + bus0.attach(1, Box::new(CountingDisk(counters[0].clone()))).unwrap(); + bus1.attach(1, Box::new(CountingDisk(counters[1].clone()))).unwrap(); + bus1.attach(2, Box::new(CountingDisk(counters[2].clone()))).unwrap(); + let r = Rig::on(Isp1040::new_12160(bus0, bus1)); + r.init_rings(16, 16); + Held { r, ch0: counters[0].clone(), ch1: counters[1].clone(), ch1_other: counters[2].clone() } + } + /// A read whose payload store the fabric refuses: the adapter keeps it outstanding, which + /// is what the abort and reset commands act on. + fn hold(&self, slot: u32, handle: u32, target: u8) { + self.r.put_entry(slot, &request(handle, target, &cdb10(scsi_cmd::READ_10, 0, 2), + &[(BUF, BLOCK_SIZE as u32), (MEM as u32, BLOCK_SIZE as u32)])); + self.r.doorbell(slot as u16 + 1); + assert_eq!(g32(&self.r.status(slot), 4), 0, "handle {handle:#x} is held, not completed"); + } + fn command(&self, regs: &[u16]) -> u16 { + let status = self.r.cmd(regs); + self.r.isp.wait_idle(); + status + } + /// The completion posted in response slot `slot`: (handle, completion code). + fn answer(&self, slot: u32) -> (u32, u16) { + let s = self.r.status(slot); + (g32(&s, 4), g16(&s, 10)) + } +} + +const CH1: u16 = 0x8000; + +/// A driver sizing its queues and recognising the part reads these before any ring exists. +#[test] +fn the_isp12160_reports_its_own_identity_firmware_and_unmapped_registers() { + let r = rig12160(); + assert_eq!(r.r(reg::ID_HIGH), 0x1216); + assert_eq!(r.r(reg::BUS_CFG0), 0, "no revision field on this part"); + r.w(reg::DMA_CMD_CFG, 0xffff); + r.w(reg::DMA_DATA_CFG, 0xffff); + assert_eq!((r.r(reg::DMA_CMD_CFG), r.r(reg::DMA_DATA_CFG)), (0, 0), "the flat ISP1040 DMA blocks are not here"); + + // Mailbox 6 is seeded because an untouched output would also read zero. + r.w(r.mb(6), 0x1234); + r.w(r.mb(0), mbox::ABOUT_FIRMWARE); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!([r.r(r.mb(1)), r.r(r.mb(2))], [10, 4]); + assert_eq!(r.r(r.mb(6)), 0, "the attribute word is cleared, not echoed"); + r.ack(); + r.w(r.mb(0), mbox::GET_FIRMWARE_STATUS); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!([r.r(r.mb(1)), r.r(r.mb(2))], [0, MAX_REQUESTS]); + r.ack(); +} + +/// The driver switches register banks through bus configuration 1 bits 9:8; each of the +/// four selections must show its own words, and no other bit may stick. +#[test] +fn the_isp12160_selects_four_distinct_banks_and_keeps_only_the_bank_field() { + let r = rig12160(); + let selections = [0x0000, 0x0100, 0x0200, 0x0300]; + for sel in selections { + r.w(reg::BUS_CFG1, sel); + r.w(reg::BANK_BASE, 0x1000 + sel); + r.w(reg::BANK_BASE + 0x20, 0x2000 + sel); + } + for sel in selections { + r.w(reg::BUS_CFG1, sel); + assert_eq!(r.r(reg::BANK_BASE), 0x1000 + sel, "bank {sel:#x}"); + assert_eq!(r.r(reg::BANK_BASE + 0x20), 0x2000 + sel, "bank {sel:#x}"); + } + r.w(reg::BUS_CFG1, 0xb2c5); + assert_eq!(r.r(reg::BUS_CFG1), 0x0200); + + // A soft reset hands a re-initialising driver empty banks, not the previous run's words. + r.w(reg::BUS_CTRL, CTRL_SOFT_RESET); + assert_eq!(r.r(reg::BUS_CFG1), 0); + for sel in selections { + r.w(reg::BUS_CFG1, sel); + assert_eq!(r.r(reg::BANK_BASE), 0, "bank {sel:#x} after reset"); + } +} + +/// A second command submitted before the driver acknowledged the first result must not +/// run, or it would overwrite the answer the driver has not read. +#[test] +fn the_isp12160_ignores_a_mailbox_submission_while_the_result_is_unacknowledged() { + let r = rig12160(); + r.w(r.mb(0), mbox::NOP); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!(r.r(reg::SEMAPHORE) & SEMAPHORE_LOCK, SEMAPHORE_LOCK); + + r.w(r.mb(1), 0x9999); + r.w(r.mb(0), mbox::ABOUT_FIRMWARE); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::ABOUT_FIRMWARE, "the second submission did not run"); + assert_eq!(r.r(r.mb(1)), 0x9999); + + r.ack(); + r.w(reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT); + assert_eq!(r.r(r.mb(0)), mbox::COMPLETE); + assert_eq!(r.r(r.mb(1)), 10, "acknowledged, the same submission runs"); + r.ack(); +} + +/// An empty slot on either channel times out on its own bus; a driver probing channel 1 +/// must see an absent target, not a hang or the channel 0 answer. +#[test] +fn a_missing_target_times_out_on_each_channel() { + let r = rig12160(); + r.init_rings(16, 16); + r.clock(0); + // An absent target answers once its selection timeout has run out on + // the adapter's clock. + let mut now = 0; + for (slot, (handle, target)) in [(0x31, 5), (0x32, 0x80 | 5)].into_iter().enumerate() { + r.put_entry(slot as u32, &request(handle, target, &TUR, &[])); + r.doorbell(slot as u16 + 1); + assert_ne!(g32(&r.status(slot as u32), 4), handle, "nothing before the selection timeout"); + now += DEFAULT_SELECTION_TIMEOUT as i64 * MS; + r.clock(now); + let s = r.status(slot as u32); + assert_eq!(g32(&s, 4), handle); + assert_eq!(g16(&s, 10), IOCB_INCOMPLETE); + assert_eq!(g16(&s, 12), STATE_GOT_BUS); + assert_eq!(g16(&s, 14), STATUS_FLAG_TIMEOUT); + } +} + +/// A command refused on channel 1 must not disturb its neighbour on channel 0, and +/// a write must land on the disk of the channel it named. +#[test] +fn a_refused_command_on_channel_1_leaves_channel_0_and_the_media_alone() { + let r = disks_on_both_channels("refuse"); + let mut seq = Seq::new(&r); + // Collect each disk's power-on unit attention so the reads below see real answers. + seq.run(0xa0, 1, &TUR, &[]); + seq.run(0xa1, 0x81, &TUR, &[]); + + r.mem.put(BUF, &[0x5c; BLOCK_SIZE]); + let beyond_disk = seq.run(0xb1, 0x81, &cdb10(scsi_cmd::READ_10, 1000, 1), &[(BUF, BLOCK_SIZE as u32)]); + assert_eq!(g16(&beyond_disk, 8) & 0xff, CHECK_CONDITION as u16); + assert_eq!(r.mem.get(BUF, BLOCK_SIZE), vec![0x5c; BLOCK_SIZE], "the refused read stored nothing"); + + let good = seq.run(0xb0, 1, &cdb10(scsi_cmd::READ_10, 0, 1), &[(BUF + 0x1000, BLOCK_SIZE as u32)]); + assert_eq!(g16(&good, 10), IOCB_COMPLETE); + assert_eq!(r.mem.get(BUF + 0x1000, BLOCK_SIZE), vec![0xa0; BLOCK_SIZE], "channel 0 still reads its own disk"); + + r.mem.put(BUF + 0x2000, &[0x77; BLOCK_SIZE]); + seq.run(0xc0, 0x81, &cdb10(scsi_cmd::WRITE_10, 3, 1), &[(BUF + 0x2000, BLOCK_SIZE as u32)]); + seq.run(0xc1, 0x81, &cdb10(scsi_cmd::READ_10, 3, 1), &[(BUF + 0x3000, BLOCK_SIZE as u32)]); + seq.run(0xc2, 1, &cdb10(scsi_cmd::READ_10, 3, 1), &[(BUF + 0x4000, BLOCK_SIZE as u32)]); + assert_eq!(r.mem.get(BUF + 0x3000, BLOCK_SIZE), vec![0x77; BLOCK_SIZE], "channel 1 holds what was written"); + assert_eq!(r.mem.get(BUF + 0x4000, BLOCK_SIZE), vec![0; BLOCK_SIZE], "channel 0's block 3 is untouched"); +} + +/// Abort device names a channel in the top bit of its selector; the same target number +/// held on the other bus must stay outstanding. +#[test] +fn abort_device_retires_only_the_named_channels_commands() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::ABORT_DEVICE, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_ABORTED)); + assert_eq!(h.answer(1), (0, 0), "channel 0's command is still held"); + // The survivor is still the adapter's to abort. + assert_eq!(h.command(&[mbox::ABORT_DEVICE, 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(1), (0x41, IOCB_ABORTED)); +} + +/// Abort command names one handle: two commands to the same channel, target and unit differ +/// only by it, and a selector that matches a held handle on the wrong channel is an error. +#[test] +fn abort_command_matches_the_handle_and_the_whole_selector() { + let h = Held::new(); + h.hold(0, 0x51, 0x81); + h.hold(1, 0x52, 0x81); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x52]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x52, IOCB_ABORTED)); + assert_eq!(h.answer(1), (0, 0), "its neighbour on the same target is untouched"); + + assert_eq!(h.command(&[mbox::ABORT_COMMAND, 0x0100, 0, 0x51]), mbox::COMMAND_ERROR, + "a held handle named on the wrong channel is not a match"); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x51]), mbox::COMPLETE); + assert_eq!(h.answer(1), (0x51, IOCB_ABORTED)); +} + +/// A driver clearing a device it already drained must read success, while naming a handle +/// nothing is using must read as an error rather than claim a retirement. +#[test] +fn aborting_nothing_is_complete_but_aborting_an_unknown_handle_is_refused() { + let h = Held::new(); + assert_eq!(h.command(&[mbox::ABORT_DEVICE, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.command(&[mbox::ABORT_COMMAND, CH1 | 0x0100, 0, 0x77]), mbox::COMMAND_ERROR); + assert_eq!(h.answer(0), (0, 0), "neither posted a completion"); +} + +/// Target reset reaches one target on one channel; the same number on the other channel and +/// the neighbour on the same channel must not see it, and the reset target keeps serving. +#[test] +fn target_reset_is_scoped_to_one_target_on_one_channel() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::ABORT_TARGET, CH1 | 0x0100, 0, 0]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_RESET)); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!([h.ch0.load(Ordering::Relaxed), h.ch1.load(Ordering::Relaxed), h.ch1_other.load(Ordering::Relaxed)], + [0, 1, 0]); + + h.r.put_entry(2, &request(0x43, 0x81, &TUR, &[])); + h.r.doorbell(3); + assert_eq!(h.answer(1), (0x43, IOCB_COMPLETE)); +} + +/// Bus reset takes its channel from mailbox 2; mailbox 1 is the reset delay and here holds a value +/// that is not even a channel. The whole named bus resets, the other one does not. +#[test] +fn bus_reset_takes_its_channel_from_mailbox_two_and_resets_every_target_on_it() { + let h = Held::new(); + h.hold(0, 0x41, 1); + h.hold(1, 0x42, 0x81); + assert_eq!(h.command(&[mbox::BUS_RESET, 3, 1]), mbox::COMPLETE); + assert_eq!(h.answer(0), (0x42, IOCB_RESET)); + assert_eq!(h.answer(1), (0, 0)); + assert_eq!([h.ch0.load(Ordering::Relaxed), h.ch1.load(Ordering::Relaxed), h.ch1_other.load(Ordering::Relaxed)], + [0, 1, 1]); + assert_eq!(h.command(&[mbox::BUS_RESET, 0, 2]), mbox::PARAM_ERROR, "there is no third bus"); +} + +/// A bus reset leaves a unit attention on the disks of that channel only; the driver's +/// first command afterwards is expected to collect it. +#[test] +fn a_bus_reset_leaves_a_unit_attention_on_the_reset_channel_only() { + let r = disks_on_both_channels("uareset"); + let mut seq = Seq::new(&r); + seq.run(0x10, 1, &TUR, &[]); + seq.run(0x11, 0x81, &TUR, &[]); + assert_eq!(g16(&seq.run(0x12, 0x81, &TUR, &[]), 8) & 0xff, 0, "attention was collected"); + + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 1]), mbox::COMPLETE); + r.isp.wait_idle(); + let after = seq.run(0x13, 0x81, &TUR, &[]); + assert_eq!(g16(&after, 8) & 0xff, CHECK_CONDITION as u16); + assert_eq!(after[34] & 0x0f, 0x06, "unit attention"); + let other = seq.run(0x14, 1, &TUR, &[]); + assert_eq!(g16(&other, 8) & 0xff, 0, "channel 0 was not reset"); + assert_eq!(g16(&seq.run(0x15, 0x81, &TUR, &[]), 8) & 0xff, 0, "reported once"); +} + // ---- the hold-off after a bus reset --------------------------------------- // // Datasheet, "Bus-reset hold-off (0x0018)": after the reset the chip starts no @@ -1651,6 +2050,87 @@ fn a_bus_reset_puts_a_disks_write_cache_back() { assert_eq!(r.mem.get(BUF + 6, 1)[0] & 0x04, 0x04, "WCE is back"); } +/// A 12160 with a cached disk at target 1 on each channel whose guests cleared WCE on both. +fn cleared_write_cache_on_both_channels(name: &str) -> Rig { + let r = rig12160(); + for channel in 0..2u8 { + let bus = r.isp.bus_for_channel(channel).unwrap(); + bus.attach_cached_image(1, &image(&format!("{name}{channel}"), 64)).expect("attach"); + bus.select_write_cache(1, false); + assert!(!bus.write_cache_reported(1), "channel {channel}: the guest cleared it"); + } + r +} + +/// The write cache bit MODE SENSE(6) of the caching page reports for target 1 on `channel`. A TUR +/// goes first: it collects the reset's unit attention, and consumes the reset flag as a guest's +/// first command does, so the mode sense that follows returns its page. +fn wce_after_next_command(seq: &mut Seq, channel: u8, handle: u32) -> bool { + let target = channel << 7 | 1; + seq.run(handle, target, &TUR, &[]); + let done = seq.run(handle + 0x100, target, &[0x1a, 0x08, 0x08, 0, 24, 0], &[(BUF, 24)]); + assert_eq!(g16(&done, 10), IOCB_COMPLETE); + assert_eq!(seq.r.mem.get(BUF + 4, 1)[0], 0x08, "caching page"); + seq.r.mem.get(BUF + 6, 1)[0] & 0x04 != 0 +} + +/// Each channel's disks put their write cache back on that channel's reset and not on the +/// other's, so the flag has to be set for the channel the mailbox names. +#[test] +fn a_12160_bus_reset_of_channel_1_restores_write_cache_on_channel_1_only() { + let r = cleared_write_cache_on_both_channels("wce12160-ch1"); + let mut seq = Seq::new(&r); + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 1]), mbox::COMPLETE); + r.isp.wait_idle(); + assert!(!wce_after_next_command(&mut seq, 0, 0x31), "channel 0 was not reset"); + assert!(wce_after_next_command(&mut seq, 1, 0x32), "channel 1 was reset"); +} + +#[test] +fn a_12160_bus_reset_of_channel_0_restores_write_cache_on_channel_0_only() { + let r = cleared_write_cache_on_both_channels("wce12160-ch0"); + let mut seq = Seq::new(&r); + assert_eq!(r.cmd(&[mbox::BUS_RESET, 0, 0]), mbox::COMPLETE); + r.isp.wait_idle(); + assert!(!wce_after_next_command(&mut seq, 1, 0x33), "channel 1 was not reset"); + assert!(wce_after_next_command(&mut seq, 0, 0x34), "channel 0 was reset"); +} + +/// Both reset the chip's SCSI buses, which the disks see as a bus reset on each channel. +#[test] +fn a_12160_chip_reset_restores_write_cache_on_both_channels() { + let r = cleared_write_cache_on_both_channels("wce12160-chip"); + r.isp.reset(); + r.isp.wait_idle(); + let mut seq = Seq::new(&r); + assert!(wce_after_next_command(&mut seq, 0, 0x35), "channel 0"); + assert!(wce_after_next_command(&mut seq, 1, 0x36), "channel 1"); +} + +#[test] +fn a_12160_soft_reset_restores_write_cache_on_both_channels() { + let r = cleared_write_cache_on_both_channels("wce12160-soft"); + r.w(reg::BUS_CTRL, CTRL_SOFT_RESET); + r.isp.wait_idle(); + let mut seq = Seq::new(&r); + assert!(wce_after_next_command(&mut seq, 0, 0x37), "channel 0"); + assert!(wce_after_next_command(&mut seq, 1, 0x38), "channel 1"); +} + +/// The single-bus part has no bus 1: a mailbox that names it is ignored, and the one bus it has +/// keeps its disks' write cache as the guest set it. +#[test] +fn an_isp1040_bus_reset_naming_bus_1_leaves_the_reset_flag_unset() { + let r = Rig::bare(); + r.isp.bus().attach_cached_image(1, &image("wce-bus1", 64)).expect("attach"); + r.init_rings(4, 4); + r.isp.bus().select_write_cache(1, false); + bus_reset(&r, 0, 1); + r.isp.wait_idle(); + assert!(!r.isp.targets_reset[0].load(Ordering::Acquire)); + assert!(!r.isp.targets_reset[1].load(Ordering::Acquire)); +} + /// A TUR for the disk at target 1. fn tur1(handle: u32) -> [u8; ENTRY_SIZE] { request(handle, 1, &[0, 0, 0, 0, 0, 0], &[]) diff --git a/src/dev/isp12160.rs b/src/dev/isp12160.rs new file mode 100644 index 000000000..9fcf55ab1 --- /dev/null +++ b/src/dev/isp12160.rs @@ -0,0 +1,338 @@ +//! The QLogic ISP12160: two SCSI buses behind one PCI function. +//! +//! The ISP12160 shares the ISP1040 ring, DMA, and mailbox mechanism, while +//! selecting targets from either channel. Each channel remains an explicit +//! [`ScsiBus`], including for monitor and run-time disc operations. +//! +//! Ported from hw/scsi/qlogic-isp.c and include/hw/scsi/qlogic-isp.h +//! in jamesbraid/qemu-sgi-origami. + +use std::collections::VecDeque; +use std::ops::Deref; + +use parking_lot::MutexGuard; + +use crate::dev::isp1040::{ + mbox, reg, Bank, Core, Isp1040, IspPart, State, CFG1_BANK_SELECT, FW_VERSION, IOCB_ABORTED, IOCB_RESET, MAILBOX_COUNT, + MAX_REQUESTS, MAX_TARGET, PCI_CLASS_SCSI, PCI_VENDOR_QLOGIC, SEMAPHORE_LOCK, STATE_ALL_PHASES, +}; +use crate::irq::IrqLine; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciRegisterPolicy, PCI_INTA}; +use crate::scsi::{ScsiBus, ScsiIdentities, ScsiIdentity}; +use crate::snapshot::{get_field, hex_u16, hex_u32, load_u16_slice, toml_u16, toml_u32, toml_u8, u16_slice_to_toml}; +use crate::traits::{Device, Resettable, Saveable}; +use crate::workstation::DiscChanger; + +const ISP_DISK: ScsiIdentity = ScsiIdentity::new(b"SGI ", b"IRIS EMULATED ", b"1.0 "); +const ISP_CDROM: ScsiIdentity = ISP_DISK; + +pub(crate) struct Isp12160Part; + +impl IspPart for Isp12160Part { + fn name(&self) -> &'static str { "ISP12160" } + fn channels(&self) -> usize { 2 } + fn pci_config(&self, bars: [Bar; 6]) -> PciConfig { + let registers = [ + PciRegisterPolicy { reg_dword: 13, initial: 0x44, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: 17, initial: 0x0001_0001, writable_mask: 0 }, + PciRegisterPolicy { reg_dword: 18, initial: 0, writable_mask: 0x100 }, + ]; + PciConfig::new_with_policy( + PCI_VENDOR_QLOGIC, + 0x1216, + PCI_CLASS_SCSI, + 6, + bars, + PCI_INTA, + PciConfigPolicy { + status: 0x02b0, + status_w1c_mask: 0, + command_mask: 0x0156, + latency_mask: 0xff, + cache_line_mask: 0xff, + registers: ®isters, + }, + ) + } + fn firmware_version(&self) -> [u16; 3] { [10, 4, FW_VERSION[2]] } + fn active_bank(&self, cfg1: u16) -> Bank { + match (cfg1 >> 8) & 3 { + 1 => Bank::Sxp, + 2 => Bank::Sxp1, + 3 => Bank::Dma, + _ => Bank::Risc, + } + } + fn checks_ram_bounds(&self) -> bool { true } + fn has_abort_commands(&self) -> bool { true } + fn target_on(&self, channel: u8, target: u8) -> u8 { target | (channel << 4) } + fn decode_target(&self, target: u8) -> (usize, u8) { ((target >> 4) as usize, target & 0x0f) } + fn id_high(&self) -> u16 { 0x1216 } + fn bus_cfg0(&self) -> u16 { 0 } + fn cfg1_write_mask(&self) -> u16 { 0x0300 } + fn unmodelled_dma_register(&self, off: u32) -> bool { + matches!(off, reg::DMA_CMD_CFG | reg::DMA_CMD_CTRL | reg::DMA_DATA_CFG | reg::DMA_DATA_CTRL) + } + fn completion_phases(&self, moved: u64, total: u64) -> u16 { + let mut flags = 0x1700; + if moved != 0 { + flags |= 0x0800; + if moved == total { flags |= 0x4000; } + } + flags + } + fn mailbox_ready(&self, semaphore: u16) -> bool { semaphore & SEMAPHORE_LOCK == 0 } + fn clear_target_parameters(&self, mailbox: &mut [u16; MAILBOX_COUNT]) { mailbox[6] = 0; } + fn set_reset_delay(&self, st: &mut State) { + st.reset_delay = st.mailbox[1]; + st.reset_delay1 = st.mailbox[2]; + } + fn reset_targets(&self, st: &mut State) -> bool { + st.reset_targets = true; + true + } + fn execute_mailbox(&self, core: &Core, st: &mut MutexGuard<'_, State>) -> bool { + let cmd = st.mailbox[0]; + match cmd { + mbox::LOAD_RAM_A64 => st.mailbox[0] = core.ram_transfer(st, true), + mbox::ABORT_COMMAND | mbox::ABORT_DEVICE | mbox::ABORT_TARGET => { + let device = st.mailbox[1]; + let valid_target = (device >> 8) & 0x7f <= MAX_TARGET as u16; + let target = ((device >> 8) & 0x7f) as u8 | (((device >> 15) as u8) << 4); + let lun = device as u8; + let handle = ((st.mailbox[2] as u32) << 16) | st.mailbox[3] as u32; + let hit = valid_target && st.active.is_some_and(|(t, l, h)| t == target + && (cmd == mbox::ABORT_TARGET || l == lun) && (cmd != mbox::ABORT_COMMAND || h == handle)); + let completion = if cmd == mbox::ABORT_TARGET { IOCB_RESET } else { IOCB_ABORTED }; + if hit { core.cancel_active(st, completion); } + let retired = core.retire_outstanding(st, |(t, l, h)| valid_target && t == target + && (cmd == mbox::ABORT_TARGET || l == lun) && (cmd != mbox::ABORT_COMMAND || h == handle), completion); + if cmd == mbox::ABORT_TARGET && valid_target { + st.pending_target_resets.push_back((target >> 4, Some(target & 15))); + } + st.mailbox[0] = if cmd == mbox::ABORT_COMMAND && !hit && retired == 0 { mbox::COMMAND_ERROR } else { mbox::COMPLETE }; + } + // Mailbox 1 is the reset delay; mailbox 2 selects the bus. + mbox::BUS_RESET => { + let channel = st.mailbox[2]; + if channel > 1 { st.mailbox[0] = mbox::PARAM_ERROR; } + else { + if st.active.is_some_and(|(target, _, _)| target >> 4 == channel as u8) { + core.cancel_active(st, IOCB_RESET); + } + core.retire_outstanding(st, |(target, _, _)| target >> 4 == channel as u8, IOCB_RESET); + st.pending_target_resets.push_back((channel as u8, None)); + core.note_bus_reset(usize::from(channel)); + core.kick(st); + st.mailbox[0] = mbox::COMPLETE; + } + } + _ => return false, + } + true + } + fn save_extra(&self, st: &State, t: &mut toml::map::Map) { + t.insert("reset_targets".into(), toml::Value::Boolean(st.reset_targets)); + t.insert("target_resets".into(), toml::Value::Array(st.pending_target_resets.iter().map(|(channel, target)| { + toml::Value::Array(vec![hex_u16(*channel as u16), hex_u16(target.unwrap_or(0xff) as u16)]) + }).collect())); + t.insert("sxp1_bank".into(), u16_slice_to_toml(&st.sxp1_bank)); + t.insert("dma_bank".into(), u16_slice_to_toml(&st.dma_bank)); + t.insert("reset_delay1".into(), hex_u16(st.reset_delay1)); + t.insert("outstanding".into(), toml::Value::Array(st.outstanding.iter().map(|&(target, lun, handle)| { + toml::Value::Array(vec![hex_u16(target as u16), hex_u16(lun as u16), hex_u32(handle)]) + }).collect())); + } + fn load_extra(&self, st: &mut State, v: &toml::Value) { + st.reset_targets = get_field(v, "reset_targets").and_then(|x| x.as_bool()).unwrap_or(false); + if let Some(toml::Value::Array(items)) = get_field(v, "target_resets") { + for item in items.iter().take(64) { + if let toml::Value::Array(fields) = item { + if fields.len() == 2 { + if let (Some(channel), Some(target)) = (toml_u8(&fields[0]), toml_u8(&fields[1])) { + if channel < 2 && (target < 16 || target == 0xff) { + st.pending_target_resets.push_back((channel, (target < 16).then_some(target))); + } + } + } + } + } + } + let u16f = |k: &str| get_field(v, k).and_then(toml_u16).unwrap_or(0); + st.reset_delay1 = u16f("reset_delay1"); + if let Some(m) = get_field(v, "sxp1_bank") { load_u16_slice(m, &mut st.sxp1_bank); } + if let Some(m) = get_field(v, "dma_bank") { load_u16_slice(m, &mut st.dma_bank); } + if let Some(toml::Value::Array(items)) = get_field(v, "outstanding") { + for item in items.iter().take(MAX_REQUESTS as usize) { + if let toml::Value::Array(fields) = item { + if fields.len() == 3 { + if let (Some(target), Some(lun), Some(handle)) = + (toml_u8(&fields[0]), toml_u8(&fields[1]), toml_u32(&fields[2])) { + if target < 32 && lun < 8 { st.outstanding.push((target, lun, handle)); } + } + } + } + } + } + } +} + +/// The two-channel controller. The shared ISP engine owns both buses. +pub struct Isp12160(Isp1040); + +impl Default for Isp12160 { + fn default() -> Self { Self::new(Self::new_bus(), Self::new_bus()) } +} + +impl Isp12160 { + /// Light the status bar's SCSI activity bits in `heartbeat`. + pub fn set_heartbeat(&self, heartbeat: std::sync::Arc) { + self.0.set_heartbeat(heartbeat); + } + + /// A 16-id bus with the ISP controller's disk and CD-ROM identities. + pub fn new_bus() -> ScsiBus { + ScsiBus::new(16, ScsiIdentities { disk: ISP_DISK, cdrom: ISP_CDROM }) + } + + /// The controller with its two channel buses already populated. + pub fn new(bus0: ScsiBus, bus1: ScsiBus) -> Self { + Self(Isp1040::new_12160(bus0, bus1)) + } + + /// Borrow one channel for the shared monitor commands. + pub fn channel(&self, channel: u8) -> Option> { + (channel < 2).then_some(Channel { adapter: self, channel }) + } + + pub fn register_locks(&self) { self.0.register_locks(); } +} + +impl Deref for Isp12160 { + type Target = Core; + fn deref(&self) -> &Core { &self.0 } +} + +/// One channel, with SCSI ids 0–15 as the monitor presents them. +pub struct Channel<'a> { + adapter: &'a Isp12160, + channel: u8, +} + +impl crate::scsi_monitor::ScsiTargets for Channel<'_> { + fn scsi_bus(&self) -> &ScsiBus { + self.adapter.bus_for_channel(self.channel).expect("valid ISP12160 channel") + } +} + +/// The machine API flattens the ISP12160's channels to ids 0–31. +/// Monitor commands use a `Channel` instead, so their ids remain 0–15. +impl DiscChanger for Isp12160 { + fn disc_status(&self) -> Vec<(usize, String, Vec, u64, u64)> { + (0..2).flat_map(|channel| { + crate::scsi_monitor::disc_status(&self.channel(channel).expect("valid ISP12160 channel")) + .into_iter() + .map(move |(id, current, discs, physical, logical)| (id + channel as usize * 16, current, discs, physical, logical)) + }).collect() + } + + fn load_disc(&self, id: usize, path: String) -> Result { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::load_disc(&self.channel(channel).unwrap(), target, path) + } + + fn eject_disc(&self, id: usize) -> Result { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::eject_disc(&self.channel(channel).unwrap(), target) + } + + fn eject_to_empty(&self, id: usize) -> Result<(), String> { + let (channel, target) = Self::split_id(id)?; + crate::scsi_monitor::eject_to_empty(&self.channel(channel).unwrap(), target) + } +} + +impl Isp12160 { + fn split_id(id: usize) -> Result<(u8, usize), String> { + if id >= 32 { return Err(format!("SCSI ID {id} is out of range")); } + Ok(((id / 16) as u8, id % 16)) + } +} + +impl Device for Isp12160 { + fn step(&self, cycles: u64) { Device::step(&self.0, cycles) } + fn start(&self) { Device::start(&self.0) } + fn stop(&self) { Device::stop(&self.0) } + fn is_running(&self) -> bool { Device::is_running(&self.0) } + fn get_clock(&self) -> u64 { Device::get_clock(&self.0) } + + fn register_commands(&self) -> Vec<(String, String)> { + let usage = crate::scsi_monitor::SCSI_USAGE; + vec![ + ("scsi".into(), format!("SCSI (QLogic ISP12160, channel 0): {usage} | scsi notes")), + ("scsi0".into(), "Alias for `scsi` (ISP12160 channel 0)".into()), + ("cow".into(), "COW overlays on ISP12160 channel 0: cow status | cow commit [id] | cow reset [id]".into()), + ("scsi1".into(), format!("SCSI (QLogic ISP12160, channel 1): {usage}")), + ("cow1".into(), "COW overlays on ISP12160 channel 1: same subcommands as `cow`".into()), + ] + } + + fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { + use std::io::Write as _; + let (channel, cow) = match cmd { + "scsi" | "scsi0" | "cow" => (0, cmd.starts_with("cow")), + "scsi1" | "cow1" => (1, cmd.starts_with("cow")), + _ => return Err("Command not found".into()), + }; + let ctl = self.channel(channel).expect("valid ISP12160 channel"); + if cow { + return crate::scsi_monitor::cow_command(&ctl, args, &mut *w); + } + if let Some(done) = crate::scsi_monitor::scsi_subcommand(&ctl, args, &mut *w) { + return done; + } + if channel == 0 && args.first().copied() == Some("notes") { + let _ = writeln!(w, "QLogic ISP12160: {} commands", self.total_commands()); + for note in self.notes() { let _ = writeln!(w, " note: {note}"); } + return Ok(()); + } + Err(format!("Usage: {}", crate::scsi_monitor::SCSI_USAGE)) + } +} + +impl Resettable for Isp12160 { + fn power_on(&self) { + self.0.reset(); + PciBusDevice::config(&self.0).reset(); + } +} + +impl Saveable for Isp12160 { + fn save_state(&self) -> toml::Value { Saveable::save_state(&self.0) } + fn load_state(&self, v: &toml::Value) -> Result<(), String> { Saveable::load_state(&self.0, v) } +} + +impl PciBusDevice for Isp12160 { + fn config(&self) -> &PciConfig { PciBusDevice::config(&self.0) } + fn bar_read(&self, bar: usize, off: u32, width: usize) -> u32 { PciBusDevice::bar_read(&self.0, bar, off, width) } + fn bar_write(&self, bar: usize, off: u32, val: u32, width: usize) { PciBusDevice::bar_write(&self.0, bar, off, val, width) } + fn connect_interrupt(&self, line: IrqLine) { PciBusDevice::connect_interrupt(&self.0, line) } + fn connect_dma(&self, dma: std::sync::Arc) { PciBusDevice::connect_dma(&self.0, dma) } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The machine disc API uses target 17 for channel 1, target 1. + #[test] + fn disc_status_flattens_the_second_channels_target_ids() { + let bus0 = Isp12160::new_bus(); + let bus1 = Isp12160::new_bus(); + bus1.attach_device(1, "", true, Vec::new(), false, None, false).unwrap(); + let isp = Isp12160::new(bus0, bus1); + + let status = DiscChanger::disc_status(&isp); + assert_eq!(status.iter().map(|drive| drive.0).collect::>(), vec![17]); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index fdd54e4b1..c8e3177b3 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,3 +60,4 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod isp12160; diff --git a/src/scsi.rs b/src/scsi.rs index bfb2ffda8..eb82e3347 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -162,6 +162,7 @@ pub struct ScsiDevice { kind: DeviceKind, buffer: Vec, // Internal buffer for WRITE_BUFFER/READ_BUFFER commands pending_sense: [u8; 18], // Stored sense data, served on REQUEST_SENSE + pending_attention: Option, /// Physical sector size: always 2048 for CD-ROM (Yellow Book), 512 for HDD. /// Used for TOC lead-out LBA (CD spec requires TOC in physical sectors). phys_block_size: u64, @@ -222,6 +223,7 @@ impl ScsiDevice { kind: if is_cdrom { DeviceKind::Cdrom } else { DeviceKind::Disk }, buffer: vec![0u8; SCSI_BUFFER_SIZE], pending_sense: [0u8; 18], + pending_attention: None, phys_block_size: if is_cdrom { CDROM_SECTOR } else { 512 }, // CD-ROM drives default to 2048-byte logical blocks (Sony CDU-76S behaviour). // HDD defaults to 512. dksc switches CD-ROM to 512 for EFS, back to 2048 for ISO. @@ -242,6 +244,7 @@ impl ScsiDevice { kind: DeviceKind::Cdrom, buffer: vec![0u8; SCSI_BUFFER_SIZE], pending_sense: [0u8; 18], + pending_attention: None, phys_block_size: CDROM_SECTOR, logical_block_size: 2048, identity, @@ -326,6 +329,10 @@ impl ScsiDevice { ScsiResponse { status: 0x02, data: vec![] } } + fn take_attention(&mut self) -> Option { + self.pending_attention.take().or_else(|| self.media.take_changed().then_some(0x28)) + } + fn exec_test_unit_ready(&mut self, _cdb: &[u8]) -> Result { if self.media.take_changed() { // Sense key 0x06 UNIT_ATTENTION, ASC 0x28 "Not Ready to Ready Transition / Medium Changed" @@ -1118,11 +1125,10 @@ impl ScsiTarget for ScsiDevice { } // Unit attention fires before any other command (except REQUEST_SENSE/INQUIRY). - if req.cdb[0] != scsi_cmd::REQUEST_SENSE - && req.cdb[0] != scsi_cmd::INQUIRY - && self.media.take_changed() - { - return Ok(self.check_condition(0x06, 0x28, 0x00)); + if req.cdb[0] != scsi_cmd::REQUEST_SENSE && req.cdb[0] != scsi_cmd::INQUIRY { + if let Some(asc) = self.take_attention() { + return Ok(self.check_condition(0x06, asc, 0x00)); + } } let mut response = match req.cdb[0] { @@ -1268,6 +1274,12 @@ impl ScsiTarget for ScsiDevice { fn block_size(&self) -> Option { Some(self.logical_block_size) } + fn bus_reset(&mut self) { + self.pending_sense = [0; 18]; + self.pending_sense[0] = 0x70; + self.pending_attention = Some(0x29); + } + fn reset_block_size(&mut self, cdrom_512: bool) { if cdrom_512 && self.is_cdrom() { self.logical_block_size = 512; @@ -1867,12 +1879,38 @@ mod tests { cd.changer().unwrap().load(&chd.display().to_string()).unwrap(); assert!(cd.has_media()); assert_eq!(cd.size(), 16 * 2048, "the CHD's size, not the file's"); - // The medium change is reported once, then the disc reads. + // A reset is reported before a media change that was already pending. + crate::scsi::ScsiTarget::bus_reset(&mut cd); + let reset = cd.request(&request(&[0, 0, 0, 0, 0, 0], None)).unwrap(); + assert_eq!(reset.status, 0x02, "unit attention after the reset"); + let reset_sense = cd.request(&request(&[0x03, 0, 0, 0, 18, 0], None)).unwrap(); + assert_eq!(reset_sense.data[12], 0x29, "reset attention comes first"); + let tur = cd.request(&request(&[0, 0, 0, 0, 0, 0], None)).unwrap(); - assert_eq!(tur.status, 0x02, "unit attention after the change"); + assert_eq!(tur.status, 0x02, "the pending media change follows"); + let sense = cd.request(&request(&[0x03, 0, 0, 0, 18, 0], None)).unwrap(); + assert_eq!(sense.data[12], 0x28, "media change keeps its ASC"); let read = cd.request(&request(&[0x28, 0, 0, 0, 0, 3, 0, 0, 1, 0], None)).unwrap(); assert_eq!(read.status, 0x00); assert_eq!(read.data, vec![0x42u8; 2048]); let _ = std::fs::remove_dir_all(dir); } + #[test] + fn bus_reset_reports_unit_attention_once_and_keeps_block_size() { + let mut dev = cdrom(); + dev.logical_block_size = 512; + dev.set_sense(0x05, 0x20, 0x00); + crate::scsi::ScsiTarget::bus_reset(&mut dev); + + let old_sense = run(&mut dev, &[0x03, 0, 0, 0, 18, 0]); + assert_eq!((old_sense.data[2], old_sense.data[12]), (0, 0), "reset clears previous sense"); + let response = run(&mut dev, &[0x00, 0, 0, 0, 0, 0]); + assert_eq!(response.status, 0x02, "a bus reset arms unit attention"); + let sense = run(&mut dev, &[0x03, 0, 0, 0, 18, 0]); + assert_eq!(sense.data[12], 0x29, "the reset attention has the reset ASC"); + + let response = run(&mut dev, &[0x00, 0, 0, 0, 0, 0]); + assert_eq!(response.status, 0x00, "the attention is reported once"); + assert_eq!(dev.block_sizes().1, 512, "bus reset leaves the selected block size alone"); + } } diff --git a/src/scsi/bus.rs b/src/scsi/bus.rs index 4283d763f..cb978d19a 100644 --- a/src/scsi/bus.rs +++ b/src/scsi/bus.rs @@ -51,8 +51,15 @@ pub trait ScsiTarget: Send { /// The logical block size, for a target that has one. fn block_size(&self) -> Option { None } - /// A controller's bus reset: block size back to what the controller - /// leaves, with a CD-ROM at 512 bytes if `cdrom_512`. + /// A SCSI bus reset reached this target, which owes the initiator a + /// unit attention on its next command. The ISP12160 calls this when the + /// guest resets either of its buses; the WD33C93A, AIC-7880 and ISP1040 + /// models never told their targets about a reset and still do not. + fn bus_reset(&mut self) {} + + /// The AIC-7880's bus reset: block size back to what that controller + /// leaves, with a CD-ROM at 512 bytes if `cdrom_512`. Only the AIC-7880 + /// calls this. fn reset_block_size(&mut self, _cdrom_512: bool) {} /// Put the mode parameters a guest can change back to what the target From a951eaf2a64c8821f3b58cb40c41d7aa65c28cc0 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:03:58 -0700 Subject: [PATCH 05/12] pci_addin: describe each machine's PCI slots in a table Which device numbers on a machine's PCI bus take an add-in card is a fact about the chassis, not something a configuration should be able to invent. Describe it once, as a const table per bus: each device number is either an onboard function the machine wires itself, or an add-in slot with how well its number is known and the public manual or driver it comes from. Only an established or observed number can be filled; the rest can be listed with what would settle them. The O2 has one add-in slot, device 3, which Linux and OpenBSD agree on and the hardware guides describe as a single PCI board position. The Octane's Bridge has four onboard devices and no add-in slot: its PCI boards hang off an XIO port behind a Bridge of their own, and neither the port nor the device numbers are established, so that table says so instead of guessing. The O2's `pci slots` monitor command prints the table with what is fitted and where each add-in pin goes. Nothing routes slot 3 yet, so it reports INTA to INTD unrouted. --- src/dev/mace_pci.rs | 92 ++++++++++++++++++++++++- src/ip30.rs | 17 +++++ src/lib.rs | 1 + src/pci_addin.rs | 162 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/pci_addin.rs diff --git a/src/dev/mace_pci.rs b/src/dev/mace_pci.rs index ae70762ef..6539b11de 100644 --- a/src/dev/mace_pci.rs +++ b/src/dev/mace_pci.rs @@ -12,6 +12,7 @@ use crate::pci::{bar_runs, plug, BarAt, BarRun, IntxTable, BarRunSink, PciBar, P use crate::snapshot::{get_field, hex_u32, load_u32_slice, toml_u32, u32_slice_to_toml}; use crate::traits::{BusDevice, BusRead32, BusRead64, Saveable, BUS_OK}; use crate::ip32::*; +use crate::pci_addin::{BusDesc, BusSlot, Evidence, SlotDesc}; pub mod macepci_reg { pub const ERROR_ADDR: u32 = 0x0000; @@ -32,6 +33,30 @@ pub const MACEPCI_REVISION_VALUE: u32 = 1; pub const IP32_SCSI_SLOT: u8 = 1; pub const IP32_SCSI_SLOT_B: u8 = 2; +/// The one add-in slot: the PCI board position in the O2's tray. Linux and +/// OpenBSD both number it device 3 and take its INTA to CRIME input 10. +pub const IP32_PCI_SLOT: u8 = 3; +pub const IP32_PCI_CRIME_INT: u32 = 10; + +/// The O2's PCI bus, for the configuration and the `pci slots` command. The +/// board's INTB to INTD wiring is not known, so those pins are left unrouted. +pub const BUSES: &[BusDesc] = &[BusDesc { + id: "pci", + title: "the O2's PCI bus", + slots: &[ + BusSlot::Onboard { device: IP32_SCSI_SLOT, name: "internal SCSI controller (AIC-7880)" }, + BusSlot::Onboard { device: IP32_SCSI_SLOT_B, name: "external SCSI controller (AIC-7880)" }, + BusSlot::AddIn(SlotDesc { + device: IP32_PCI_SLOT, + label: "", + evidence: Evidence::Established, + source: "Linux arch/mips/pci/fixup-ip32.c (irq_tab_mace); OpenBSD sys/arch/sgi/pci/macepcibridge.c (mace_pcibr_intr_map); O2 Workstation Hardware Reference Guide 007-3275-003, installing a PCI board", + settled_by: "", + }), + ], + no_add_in: "", +}]; + /// Which of CRIME's interrupt inputs the pin of the function in that slot /// drives. NetBSD works this out for itself and prints it: "interrupting at /// crime interrupt 8". @@ -325,6 +350,42 @@ impl MacePci { Some((s.name, s.dev.clone(), touched)) } + /// The bus's slots from [`BUSES`], with what is in each and where its + /// interrupt pins go: one line per device number the table knows. + pub fn slots_report(&self) -> Vec { + let bus = &BUSES[0]; + let mut out = vec![format!("{}:", bus.title)]; + for row in bus.slots { + let device = row.device(); + let fitted = self.slot(device).map(|(name, f, _)| { + let id = f.config().read(0); + format!("{name} {:04x}:{:04x}", id & 0xffff, id >> 16) + }); + let line = match row { + BusSlot::Onboard { name, .. } => { + format!(" device {device}: onboard, {name}") + } + BusSlot::AddIn(s) => { + let pins: Vec = (1..=4u8) + .map(|p| match self.irq.input(device, p) { + Some(i) => format!("INT{} CRIME {i}", (b'A' + p - 1) as char), + None => format!("INT{} unrouted", (b'A' + p - 1) as char), + }) + .collect(); + let label = if s.label.is_empty() { String::new() } else { format!(" \"{}\"", s.label) }; + format!( + " device {device}{label}: add-in slot ({}), {}; {}", + s.evidence.name(), + fitted.as_deref().map_or("empty".to_string(), |f| format!("holds {f}")), + pins.join(", ") + ) + } + }; + out.push(line); + } + out + } + /// Whether `addr` is in one of the CPU's windows onto PCI memory. pub fn in_window(addr: u32) -> bool { (PCI_MEM_BASE..PCI_MEM_BASE + PCI_MEM_SIZE).contains(&addr) @@ -582,7 +643,7 @@ impl crate::traits::Device for MacePci { fn get_clock(&self) -> u64 { 0 } fn register_commands(&self) -> Vec<(String, String)> { - vec![("pci".to_string(), "MACE PCI bridge: pci status | pci config ".to_string())] + vec![("pci".to_string(), "MACE PCI bridge: pci status | pci slots | pci config ".to_string())] } fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { @@ -612,6 +673,11 @@ impl crate::traits::Device for MacePci { s.name, id & 0xffff, id >> 16, c.command(), bars.join(", ")).ok(); } } + "slots" => { + for line in self.slots_report() { + writeln!(w, "{line}").ok(); + } + } "config" => { let slot: u8 = args.get(1).and_then(|v| v.parse().ok()).ok_or("Usage: pci config ")?; let (_, f, _) = self.slot(slot).ok_or(format!("pci: nothing in slot {slot}"))?; @@ -620,7 +686,7 @@ impl crate::traits::Device for MacePci { writeln!(w, " {:02x}: {}", row * 16, v.join(" ")).ok(); } } - _ => return Err("Usage: pci status | config ".to_string()), + _ => return Err("Usage: pci status | slots | config ".to_string()), } Ok(()) } @@ -843,4 +909,26 @@ mod tests { c.irq.get().unwrap().set(true); assert_eq!(*crime.0.lock().unwrap(), vec![(IP32_SCSI_CRIME_INT, true), (IP32_SCSI_CRIME_INT_B, true)]); } + + /// The table's onboard rows are the slots the machine attaches, and its + /// one add-in slot is where nothing is attached. + #[test] + fn the_bus_table_matches_the_onboard_slots() { + let onboard: Vec = BUSES[0].slots.iter() + .filter_map(|s| match s { BusSlot::Onboard { device, .. } => Some(*device), _ => None }) + .collect(); + assert_eq!(onboard, vec![IP32_SCSI_SLOT, IP32_SCSI_SLOT_B]); + let add_in: Vec = BUSES[0].add_ins().map(|s| s.device).collect(); + assert_eq!(add_in, vec![IP32_PCI_SLOT]); + } + + #[test] + fn pci_slots_lists_every_device_the_table_knows() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime, test_ram()); + let report = pci.slots_report(); + assert_eq!(report.len(), 1 + BUSES[0].slots.len()); + assert!(report[1].contains("device 1: onboard"), "{report:?}"); + assert!(report[3].contains("device 3: add-in slot (established), empty"), "{report:?}"); + } } diff --git a/src/ip30.rs b/src/ip30.rs index 6fd83fdbd..38858aa94 100644 --- a/src/ip30.rs +++ b/src/ip30.rs @@ -31,6 +31,7 @@ use parking_lot::Mutex; use crate::dev::heart_mem::HeartRam; use crate::pci::BarRun; +use crate::pci_addin::{BusDesc, BusSlot}; use crate::physical::Physical; use crate::ppmem::devmap::{entry_eq, SLOT_SHIFT}; use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, BUS_ERR, BUS_OK}; @@ -40,6 +41,22 @@ pub const WIDGET_XBOW: u8 = 0x0; pub const WIDGET_HEART: u8 = 0x8; pub const WIDGET_BRIDGE: u8 = 0xf; +/// The PCI bus on the Bridge, for the configuration. All four devices are +/// onboard; the Octane's PCI boards sit behind their own Bridge on an XIO +/// port, which this table does not describe until its port and device +/// numbers are established. +pub const BUSES: &[BusDesc] = &[BusDesc { + id: "bridge", + title: "the Octane's Bridge", + slots: &[ + BusSlot::Onboard { device: 0, name: "internal SCSI controller" }, + BusSlot::Onboard { device: 1, name: "external SCSI controller" }, + BusSlot::Onboard { device: 2, name: "IOC3 (Ethernet, serial, real-time clock)" }, + BusSlot::Onboard { device: 3, name: "RAD1 audio" }, + ], + no_add_in: "the Octane's PCI boards sit on XIO ports behind a Bridge of their own, and their port and device numbers are not established", +}]; + /// Widget `w`'s small window: 16 MiB at `0x1000_0000 + (w << 24)`. pub const fn widget_small_base(w: u8) -> u64 { 0x1000_0000 | (w as u64) << 24 diff --git a/src/lib.rs b/src/lib.rs index c4b68b397..2e3ab5d4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,6 +199,7 @@ pub mod scsi_monitor; pub mod irq; pub mod host_audio; pub mod pci; +pub mod pci_addin; pub mod ui; pub mod cpu; pub mod dev; diff --git a/src/pci_addin.rs b/src/pci_addin.rs new file mode 100644 index 000000000..89d11d1c3 --- /dev/null +++ b/src/pci_addin.rs @@ -0,0 +1,162 @@ +//! Which PCI slots a machine has, and which of them take an add-in card. +//! +//! A machine's PCI buses are described by `const` tables: a bus id, and for +//! each device number on it either an onboard function (hard-wired by the +//! machine, never configurable) or an add-in slot. A row says how well its +//! device number is known and where that comes from. Only a slot whose +//! number is established or observed can be filled; the others are listed +//! so an error can say what is missing instead of "no such slot". +//! +//! Slots are named the way a machine's PCI bus numbers them, by device +//! number. A slot can also carry the label printed on the chassis; labels +//! are only a way to write the same device number in the configuration. + +use crate::config::MachineProfile; + +/// How well a fact is known, from strongest to weakest. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Evidence { + /// A manual, or software that must agree with the hardware. + Established, + /// Seen on a real machine. + Observed, + /// Follows from a neighbouring fact, not seen. + Inferred, + /// A modelling choice that behaves like the hardware where tested. + Approximation, + /// No source says. + Unknown, +} + +impl Evidence { + /// A slot can be configured only when its device number is known. + pub fn selectable(self) -> bool { + matches!(self, Self::Established | Self::Observed) + } + + pub fn name(self) -> &'static str { + match self { + Self::Established => "established", + Self::Observed => "observed", + Self::Inferred => "inferred", + Self::Approximation => "approximation", + Self::Unknown => "unknown", + } + } +} + +/// A slot that takes an add-in card. +#[derive(Debug)] +pub struct SlotDesc { + /// The PCI device number (the IDSEL line) on the bus. + pub device: u8, + /// What the chassis calls the slot, or "" when it carries no label. + pub label: &'static str, + /// How well `device` is known. + pub evidence: Evidence, + /// The public manual or driver the number comes from. + pub source: &'static str, + /// What would make an unselectable slot selectable. + pub settled_by: &'static str, +} + +/// One device number on a bus. +#[derive(Debug)] +pub enum BusSlot { + /// A function the machine always has, wired by the machine. + Onboard { device: u8, name: &'static str }, + AddIn(SlotDesc), +} + +impl BusSlot { + pub fn device(&self) -> u8 { + match self { + Self::Onboard { device, .. } => *device, + Self::AddIn(s) => s.device, + } + } +} + +/// A PCI bus and the device numbers that are known on it. +#[derive(Debug)] +pub struct BusDesc { + /// The id a configuration names the bus by. + pub id: &'static str, + /// The bus as an error message names it, "the O2's PCI bus". + pub title: &'static str, + /// Ascending by device number. + pub slots: &'static [BusSlot], + /// Why the bus has no add-in slot, when it has none. + pub no_add_in: &'static str, +} + +impl BusDesc { + pub fn add_ins(&self) -> impl Iterator { + self.slots.iter().filter_map(|s| match s { + BusSlot::AddIn(d) => Some(d), + BusSlot::Onboard { .. } => None, + }) + } + + /// The add-in slots as an error lists them: `3`, or `5 (label)`. + pub fn add_in_list(&self) -> String { + let all: Vec = self + .add_ins() + .map(|s| if s.label.is_empty() { s.device.to_string() } else { format!("{} ({})", s.device, s.label) }) + .collect(); + if all.is_empty() { "none".into() } else { all.join(", ") } + } +} + +/// The PCI buses of a machine, empty for one with no PCI. +pub fn buses(profile: MachineProfile) -> &'static [BusDesc] { + match profile { + MachineProfile::O2Ip32 => crate::ip32::BUSES, + MachineProfile::OctaneIp30 => crate::ip30::BUSES, + MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 => &[], + } +} + +/// How a message names the machine. +pub fn machine_noun(profile: MachineProfile) -> &'static str { + match profile { + MachineProfile::IndyIp24 => "the Indy", + MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 => "the Indigo2", + MachineProfile::O2Ip32 => "the O2", + MachineProfile::OctaneIp30 => "the Octane", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_table_is_ascending_and_every_slot_is_sourced() { + for profile in MachineProfile::EVERY { + for bus in buses(profile) { + let devices: Vec = bus.slots.iter().map(BusSlot::device).collect(); + let mut sorted = devices.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(devices, sorted, "{}: devices ascending and unique", bus.id); + assert!(devices.iter().all(|&d| d < 32), "{}: a bus has 32 devices", bus.id); + for s in bus.add_ins() { + assert!(!s.source.is_empty(), "{} slot {}: no source", bus.id, s.device); + if !s.evidence.selectable() { + assert!(!s.settled_by.is_empty(), "{} slot {}: say what settles it", bus.id, s.device); + } + } + assert!(bus.add_ins().count() > 0 || !bus.no_add_in.is_empty(), "{}: a bus with no slot says why", bus.id); + } + } + } + + #[test] + fn only_the_o2_has_an_add_in_slot_today() { + for profile in MachineProfile::EVERY { + let slots: usize = buses(profile).iter().map(|b| b.add_ins().count()).sum(); + assert_eq!(slots, usize::from(profile == MachineProfile::O2Ip32), "{}", profile.name()); + } + } +} From 2b72c992f626316767aeb4730bb0e20c692f4df8 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:04:46 -0700 Subject: [PATCH 06/12] pci_cards: add the card registry and the ISP12160 card A fitted card needs more from a machine than a slot: the SCSI buses it brings, a worker to start and stop, state to save. Give every card the same small interface and list the kinds in one static table, each entry building a fresh card, so a machine can fit several of the same kind. Nothing here depends on a particular bus. The first card is a dual-channel PCI SCSI adapter built on the QLogic ISP12160. It reuses the ISP12160 model added just before it in this series and brings two wide SCSI buses. It takes no options: nothing about the part is configurable on the board. --- src/lib.rs | 1 + src/pci_cards.rs | 150 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 src/pci_cards.rs diff --git a/src/lib.rs b/src/lib.rs index 2e3ab5d4a..fc4f4d215 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,7 @@ pub mod irq; pub mod host_audio; pub mod pci; pub mod pci_addin; +pub mod pci_cards; pub mod ui; pub mod cpu; pub mod dev; diff --git a/src/pci_cards.rs b/src/pci_cards.rs new file mode 100644 index 000000000..92b0c3325 --- /dev/null +++ b/src/pci_cards.rs @@ -0,0 +1,150 @@ +//! The add-in cards a PCI slot can be fitted with. +//! +//! A card is a PCI function plus whatever the machine has to do for it +//! beyond giving it a slot: the SCSI buses it brings, a thread to start +//! and stop, state to save. The list of cards is a static table; each +//! entry builds a fresh card on every use, so a machine can fit several +//! of the same kind. + +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +use crate::dev::isp12160::Isp12160; +use crate::pci::PciBusDevice; +use crate::scsi::ScsiBus; +use crate::traits::{Device, Resettable, Saveable}; + +/// The `key = value` pairs of a `[[pci]]` entry beyond `slot`, `bus` and +/// `card`. +pub type Options = BTreeMap; + +/// A card fitted in a slot. +pub trait PciCard: Send + Sync { + fn name(&self) -> &'static str; + + /// The card's PCI function, which the bus attaches. + fn function(&self) -> Arc; + + /// The SCSI buses the card brings, in channel order. The machine numbers + /// them as controllers after its own. + fn scsi_buses(&self) -> Vec<&ScsiBus> { + Vec::new() + } + + /// Light the status bar's activity bits in `heartbeat`. + fn set_heartbeat(&self, _heartbeat: Arc) {} + + /// Name the card's locks in the lock registry. + fn register_locks(&self) {} + + /// Start the card's worker, if it has one. Stop finishes the command it + /// is running, so a snapshot finds none half done. + fn start(&self) {} + fn stop(&self) {} + + fn power_on(&self) {} + fn save_state(&self) -> toml::Value; + fn load_state(&self, v: &toml::Value) -> Result<(), String>; +} + +/// Build a card from its options, given the controller number its first SCSI +/// bus will have. +pub type BuildCard = fn(&Options, u8) -> Result, String>; + +/// One kind of card. +pub struct CardDef { + /// The name a `[[pci]]` entry gives as `card`. + pub name: &'static str, + pub summary: &'static str, + /// The option keys the card takes. + pub options: &'static [&'static str], + /// How many SCSI controllers (buses) it adds to the machine's. + pub scsi_controllers: u8, + pub build: BuildCard, +} + +pub static CARDS: &[CardDef] = &[CardDef { + name: "isp12160", + summary: "QLogic ISP12160, two Ultra160 SCSI buses on one PCI function", + options: &[], + scsi_controllers: 2, + build: |_, _| Ok(Box::new(IspCard(Arc::new(Isp12160::default())))), +}]; + +pub fn card_def(name: &str) -> Option<&'static CardDef> { + CARDS.iter().find(|c| c.name == name) +} + +/// The card names, for an error message. +pub fn card_names() -> String { + CARDS.iter().map(|c| c.name).collect::>().join(", ") +} + +struct IspCard(Arc); + +impl PciCard for IspCard { + fn name(&self) -> &'static str { + "isp12160" + } + + fn function(&self) -> Arc { + self.0.clone() + } + + fn scsi_buses(&self) -> Vec<&ScsiBus> { + (0..2).filter_map(|channel| self.0.bus_for_channel(channel)).collect() + } + + fn set_heartbeat(&self, heartbeat: Arc) { + self.0.set_heartbeat(heartbeat); + } + + fn register_locks(&self) { + self.0.register_locks(); + } + + fn start(&self) { + Device::start(&*self.0); + } + + fn stop(&self) { + Device::stop(&*self.0); + } + + fn power_on(&self) { + Resettable::power_on(&*self.0); + } + + fn save_state(&self) -> toml::Value { + Saveable::save_state(&*self.0) + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + Saveable::load_state(&*self.0, v) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_isp12160_brings_two_buses_and_a_qlogic_function() { + let def = card_def("isp12160").expect("registered"); + let card = (def.build)(&Options::new(), 2).unwrap(); + assert_eq!(card.scsi_buses().len(), def.scsi_controllers as usize); + let id = card.function().config().read(0); + assert_eq!((id & 0xffff, id >> 16), (0x1077, 0x1216), "QLogic ISP12160"); + assert!(card_def("rad9").is_none()); + } + + #[test] + fn two_cards_do_not_share_buses_or_state() { + let def = card_def("isp12160").unwrap(); + let (a, b) = ((def.build)(&Options::new(), 2).unwrap(), (def.build)(&Options::new(), 2).unwrap()); + let (ba, bb) = (a.scsi_buses()[0] as *const ScsiBus, b.scsi_buses()[0] as *const ScsiBus); + assert_ne!(ba, bb); + b.load_state(&a.save_state()).expect("the same card's state loads"); + } +} From dd819c2e7016937d3f5ab75a5c8da7be1331d250 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:07:28 -0700 Subject: [PATCH 07/12] pci_addin: parse and validate card placements Turn a card entry into a placement or a message that says what to fix. An entry names a bus (optional on a machine with one), a slot, a card and the card's own options. The slot is a PCI device number or the label printed on the chassis; labels are resolved here and nowhere else, so the buses, snapshots and the monitor only ever see device numbers. Checks run in the order a reader would fix them: the machine has PCI, the bus exists, the slot is an add-in slot and its number is known, the slot is free, the card exists, its options are ones it takes. Onboard devices are named in the message instead of reported as missing, because they are real and cannot be replaced. Several cards of one kind are allowed; their SCSI buses are numbered after the machine's own, in entry order. The command-line form is `[BUS:]SLOT=CARD[,KEY=VALUE]...`. An entry on the command line replaces a configuration entry for the same slot. --- src/pci_addin.rs | 310 +++++++++++++++++++++++++++++++++++++++++++++++ src/pci_cards.rs | 1 + 2 files changed, 311 insertions(+) diff --git a/src/pci_addin.rs b/src/pci_addin.rs index 89d11d1c3..531d3f3fe 100644 --- a/src/pci_addin.rs +++ b/src/pci_addin.rs @@ -11,7 +11,10 @@ //! number. A slot can also carry the label printed on the chassis; labels //! are only a way to write the same device number in the configuration. +use serde::{Deserialize, Serialize}; + use crate::config::MachineProfile; +use crate::pci_cards::{card_def, card_names, CardDef, Options}; /// How well a fact is known, from strongest to weakest. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -127,6 +130,180 @@ pub fn machine_noun(profile: MachineProfile) -> &'static str { } } +/// A slot as a configuration names it: the PCI device number, or the label +/// on the chassis. Labels are only a spelling of a device number; they are +/// resolved here and appear nowhere else. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SlotRef { + Device(u8), + Label(String), +} + +/// One `[[pci]]` entry: a card in a slot, with the card's own options inline. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PciEntry { + pub slot: SlotRef, + /// The bus id, left out on a machine with one PCI bus. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bus: Option, + pub card: String, + /// Every other key, for the card to read. + #[serde(flatten)] + pub options: Options, +} + +impl PciEntry { + /// Parse the command-line form `[BUS:]SLOT=CARD[,KEY=VALUE]...`. SLOT is + /// a PCI device number: chassis labels are for the configuration file. + /// Option values are booleans + /// and integers where they read as one, text otherwise. + pub fn parse_cli(text: &str) -> Result { + let usage = "expected [BUS:]SLOT=CARD[,KEY=VALUE]..."; + let (place, rest) = text.split_once('=').ok_or_else(|| format!("pci \"{text}\": {usage}"))?; + let (bus, slot) = match place.split_once(':') { + Some((b, s)) if !b.is_empty() => (Some(b.to_string()), s), + Some((_, s)) => (None, s), + None => (None, place), + }; + if slot.is_empty() { + return Err(format!("pci \"{text}\": the slot is empty ({usage})")); + } + let slot = slot.parse::().map(SlotRef::Device).map_err(|_| { + format!("pci \"{text}\": slot \"{slot}\" is not a PCI device number (0 to 255); chassis labels work only in the configuration file") + })?; + let mut parts = rest.split(','); + let card = parts.next().filter(|c| !c.is_empty()).ok_or_else(|| format!("pci \"{text}\": the card is empty ({usage})"))?; + let mut options = Options::new(); + for kv in parts { + let (k, v) = kv.split_once('=').filter(|(k, _)| !k.is_empty()).ok_or_else(|| format!("pci \"{text}\": option \"{kv}\" is not KEY=VALUE"))?; + let value = match v { + "true" => toml::Value::Boolean(true), + "false" => toml::Value::Boolean(false), + _ => v.parse::().map_or_else(|_| toml::Value::String(v.to_string()), toml::Value::Integer), + }; + if options.insert(k.to_string(), value).is_some() { + return Err(format!("pci \"{text}\": option \"{k}\" is given twice")); + } + } + Ok(Self { slot, bus, card: card.to_string(), options }) + } +} + +/// An entry that passed [`validate_pci`], with everything it resolved to. +#[derive(Debug)] +pub struct Placement { + pub bus: &'static BusDesc, + pub slot: &'static SlotDesc, + pub card: &'static CardDef, + pub options: Options, + /// The controller number of the card's first SCSI bus: after the + /// machine's own, and after the cards before it. + pub scsi_base: u8, +} + +/// The bus an entry names, or the machine's only one. +fn find_bus<'a>(all: &'a [BusDesc], noun: &str, bus: Option<&str>) -> Result<&'a BusDesc, String> { + let ids = || all.iter().map(|b| b.id).collect::>().join(", "); + match (bus, all) { + (_, []) => Err(format!("{noun} has no PCI bus.")), + (Some(id), _) => all.iter().find(|b| b.id == id).ok_or_else(|| format!("{noun} has no PCI bus \"{id}\" (buses: {}).", ids())), + (None, [only]) => Ok(only), + (None, _) => Err(format!("{noun} has several PCI buses ({}); set bus.", ids())), + } +} + +/// The row a slot names on `bus`: a device number, or an add-in slot's label. +fn find_row<'a>(bus: &'a BusDesc, slot: &SlotRef) -> Option<&'a BusSlot> { + match slot { + SlotRef::Device(n) => bus.slots.iter().find(|s| s.device() == *n), + SlotRef::Label(l) => bus.slots.iter().find(|s| matches!(s, BusSlot::AddIn(d) if !d.label.is_empty() && d.label == l)), + } +} + +/// Check every `[[pci]]` entry against the machine's slot tables and the +/// card registry, in the order a reader would fix them: bus, slot, whether +/// the slot is free, card, options. The first problem ends the check. +pub fn validate_pci(profile: MachineProfile, entries: &[PciEntry]) -> Result, String> { + place(buses(profile), machine_noun(profile), profile.scsi_controllers(), entries) +} + +/// [`validate_pci`] for any table of buses, `scsi_base` being the first +/// controller number free for a card's SCSI buses. +fn place(all: &'static [BusDesc], noun: &str, mut scsi_base: u8, entries: &[PciEntry]) -> Result, String> { + let mut out: Vec = Vec::new(); + let mut used: Vec<(&'static str, u8, usize)> = Vec::new(); + for (i, e) in entries.iter().enumerate() { + let at = format!("pci[{i}]"); + let bus = find_bus(all, noun, e.bus.as_deref()).map_err(|m| format!("{at}: {m}"))?; + if bus.add_ins().count() == 0 { + return Err(format!("{at}: {} has no add-in slot: {}.", bus.title, bus.no_add_in)); + } + let slot = match find_row(bus, &e.slot) { + Some(BusSlot::AddIn(s)) => s, + Some(BusSlot::Onboard { device, name }) => { + return Err(format!( + "{at}: device {device} on {} is the onboard {name}, not an add-in slot. Add-in slots: {}.", + bus.title, + bus.add_in_list() + )); + } + None => { + let named = match &e.slot { + SlotRef::Device(n) => format!("device {n} on {} is not a slot", bus.title), + SlotRef::Label(l) if l.parse::().is_ok() => { + return Err(format!( + "{at}: slot \"{l}\" is text, so it names a chassis label, and {} has no such label. For device {l} write slot = {l} without quotes.", + bus.title + )); + } + SlotRef::Label(l) => format!("{} has no slot labelled \"{l}\"", bus.title), + }; + return Err(format!("{at}: {named}. Add-in slots: {}. `pci slots` in the monitor lists them.", bus.add_in_list())); + } + }; + if !slot.evidence.selectable() { + return Err(format!( + "{at}: slot {} on {} cannot be configured: its device number is {}. {}", + slot.device, + bus.title, + slot.evidence.name(), + slot.settled_by + )); + } + if let Some(&(_, _, j)) = used.iter().find(|&&(b, d, _)| b == bus.id && d == slot.device) { + return Err(format!("{at}: {} slot {} is already used by pci[{j}] (card \"{}\").", bus.id, slot.device, entries[j].card)); + } + let card = card_def(&e.card).ok_or_else(|| format!("{at}: unknown card \"{}\" (cards: {}).", e.card, card_names()))?; + if let Some(k) = e.options.keys().find(|k| !card.options.contains(&k.as_str())) { + let known = if card.options.is_empty() { "this card takes no options".to_string() } else { format!("known: {}", card.options.join(", ")) }; + return Err(format!("{at} card \"{}\": unknown option \"{k}\" ({known}).", card.name)); + } + used.push((bus.id, slot.device, i)); + out.push(Placement { bus, slot, card, options: e.options.clone(), scsi_base }); + scsi_base = scsi_base.saturating_add(card.scsi_controllers); + } + Ok(out) +} + +/// Add the command line's entries to the configuration's: an entry for a +/// slot already filled there replaces it in place, any other is appended. +/// Entries that do not resolve are appended and left for [`validate_pci`] +/// to report. +pub fn merge_cli(profile: MachineProfile, base: &mut Vec, cli: &[PciEntry]) { + let identity = |e: &PciEntry| -> Option<(&'static str, u8)> { + let bus = find_bus(buses(profile), machine_noun(profile), e.bus.as_deref()).ok()?; + Some((bus.id, find_row(bus, &e.slot)?.device())) + }; + for e in cli { + let id = identity(e); + match base.iter_mut().find(|b| id.is_some() && identity(b) == id) { + Some(slot) => *slot = e.clone(), + None => base.push(e.clone()), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -159,4 +336,137 @@ mod tests { assert_eq!(slots, usize::from(profile == MachineProfile::O2Ip32), "{}", profile.name()); } } + fn entry(text: &str) -> PciEntry { + PciEntry::parse_cli(text).unwrap() + } + + /// An entry that names its slot by label, as only a config file can. + fn labelled(bus: &str, label: &str, card: &str) -> PciEntry { + PciEntry { slot: SlotRef::Label(label.into()), bus: Some(bus.into()), card: card.into(), options: Options::new() } + } + + fn o2(texts: &[&str]) -> Result, String> { + validate_pci(MachineProfile::O2Ip32, &texts.iter().map(|t| entry(t)).collect::>()) + } + + /// A bus with labelled slots in each state, for what the real tables + /// cannot show yet: more than one slot, labels, an unsettled number. + static TEST_BUSES: [BusDesc; 2] = [ + BusDesc { + id: "a", + title: "bus a", + slots: &[ + BusSlot::Onboard { device: 1, name: "disk controller" }, + BusSlot::AddIn(SlotDesc { device: 5, label: "slot 1", evidence: Evidence::Established, source: "test", settled_by: "" }), + BusSlot::AddIn(SlotDesc { device: 6, label: "slot 2", evidence: Evidence::Observed, source: "test", settled_by: "" }), + BusSlot::AddIn(SlotDesc { device: 7, label: "slot 3", evidence: Evidence::Unknown, source: "test", settled_by: "a capture with a card in it" }), + ], + no_add_in: "", + }, + // Device 5 again, on another bus: a slot is a bus and a device number. + BusDesc { id: "b", title: "bus b", slots: &[BusSlot::AddIn(SlotDesc { device: 5, label: "", evidence: Evidence::Established, source: "test", settled_by: "" })], no_add_in: "" }, + ]; + + fn test_bus(texts: &[&str]) -> Result, String> { + place(&TEST_BUSES, "the test machine", 2, &texts.iter().map(|t| entry(t)).collect::>()) + } + + #[test] + fn the_o2s_slot_takes_a_card_and_numbers_its_scsi_buses_after_the_onboard_two() { + let p = o2(&["3=isp12160"]).unwrap(); + assert_eq!((p[0].bus.id, p[0].slot.device, p[0].card.name, p[0].scsi_base), ("pci", 3, "isp12160", 2)); + } + + #[test] + fn cli_form_parses_bus_slot_card_and_options() { + let e = entry("e:2=isp12160,depth=4,fast=true,mode=wide"); + assert_eq!((e.bus.as_deref(), &e.slot, e.card.as_str()), (Some("e"), &SlotRef::Device(2), "isp12160")); + assert_eq!(e.options["depth"], toml::Value::Integer(4)); + assert_eq!(e.options["fast"], toml::Value::Boolean(true)); + assert_eq!(e.options["mode"], toml::Value::String("wide".into())); + for bad in ["isp12160", "3=", "=isp12160", "3=isp12160,x", "3=isp12160,a=1,a=2", ":=x"] { + assert!(PciEntry::parse_cli(bad).is_err(), "{bad}"); + } + } + + #[test] + fn the_command_line_takes_device_numbers_only() { + for bad in ["left=isp12160", "300=isp12160", "a:slot 1=isp12160"] { + let err = PciEntry::parse_cli(bad).unwrap_err(); + assert!(err.contains("is not a PCI device number") && err.contains("only in the configuration file"), "{bad}: {err}"); + } + } + + #[test] + fn a_label_names_the_same_slot_as_its_device_number() { + let by_label = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "slot 2", "isp12160")]).unwrap(); + let by_device = test_bus(&["a:6=isp12160"]).unwrap(); + assert_eq!(by_label[0].slot.device, 6); + assert_eq!(by_label[0].slot.device, by_device[0].slot.device); + let err = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "slot 9", "isp12160")]).unwrap_err(); + assert_eq!(err, "pci[0]: bus a has no slot labelled \"slot 9\". Add-in slots: 5 (slot 1), 6 (slot 2), 7 (slot 3). `pci slots` in the monitor lists them."); + } + + #[test] + fn two_cards_of_one_kind_get_consecutive_scsi_controllers() { + let p = test_bus(&["a:5=isp12160", "a:6=isp12160", "b:5=isp12160"]).unwrap(); + assert_eq!(p.iter().map(|p| p.scsi_base).collect::>(), vec![2, 4, 6]); + } + + #[test] + fn a_slot_cannot_be_used_twice_but_another_bus_is_free() { + let by_label = [entry("a:5=isp12160"), labelled("a", "slot 1", "isp12160")]; + let err = place(&TEST_BUSES, "the test machine", 2, &by_label).unwrap_err(); + assert_eq!(err, "pci[1]: a slot 5 is already used by pci[0] (card \"isp12160\")."); + let err = test_bus(&["a:5=isp12160", "a:5=aic7880"]).unwrap_err(); + assert!(err.contains("already used by pci[0]"), "{err}"); + test_bus(&["a:5=isp12160", "b:5=isp12160"]).expect("device 5 on the other bus is another slot"); + } + + #[test] + fn errors_say_what_is_wrong_and_what_to_do() { + assert_eq!( + o2(&["7=isp12160"]).err().unwrap(), + "pci[0]: device 7 on the O2's PCI bus is not a slot. Add-in slots: 3. `pci slots` in the monitor lists them." + ); + assert_eq!( + o2(&["1=isp12160"]).err().unwrap(), + "pci[0]: device 1 on the O2's PCI bus is the onboard internal SCSI controller (AIC-7880), not an add-in slot. Add-in slots: 3." + ); + assert_eq!(o2(&["3=rad9"]).err().unwrap(), "pci[0]: unknown card \"rad9\" (cards: isp12160)."); + assert_eq!( + o2(&["3=isp12160,volume=3"]).err().unwrap(), + "pci[0] card \"isp12160\": unknown option \"volume\" (this card takes no options)." + ); + assert_eq!( + test_bus(&["a:7=isp12160"]).err().unwrap(), + "pci[0]: slot 7 on bus a cannot be configured: its device number is unknown. a capture with a card in it" + ); + assert_eq!(test_bus(&["5=isp12160"]).err().unwrap(), "pci[0]: the test machine has several PCI buses (a, b); set bus."); + assert_eq!(test_bus(&["c:5=isp12160"]).err().unwrap(), "pci[0]: the test machine has no PCI bus \"c\" (buses: a, b)."); + assert_eq!(o2(&["pci:3=isp12160"]).unwrap().len(), 1); + let quoted = place(&TEST_BUSES, "the test machine", 2, &[labelled("a", "6", "isp12160")]).unwrap_err(); + assert_eq!( + quoted, + "pci[0]: slot \"6\" is text, so it names a chassis label, and bus a has no such label. For device 6 write slot = 6 without quotes." + ); + } + + #[test] + fn a_machine_without_an_add_in_slot_says_why() { + let indy = validate_pci(MachineProfile::IndyIp24, &[entry("3=isp12160")]).err().unwrap(); + assert_eq!(indy, "pci[0]: the Indy has no PCI bus."); + let octane = validate_pci(MachineProfile::OctaneIp30, &[entry("4=isp12160")]).err().unwrap(); + assert!(octane.starts_with("pci[0]: the Octane's Bridge has no add-in slot: "), "{octane}"); + } + + #[test] + fn a_command_line_entry_replaces_the_config_entry_for_its_slot() { + let mut base = vec![entry("3=isp12160")]; + merge_cli(MachineProfile::O2Ip32, &mut base, &[entry("pci:3=isp12160"), entry("9=isp12160")]); + assert_eq!(base.len(), 2); + assert_eq!(base[0].bus.as_deref(), Some("pci"), "replaced in place"); + assert_eq!(base[1].slot, SlotRef::Device(9), "an entry that names no slot is kept for validate to report"); + } + } diff --git a/src/pci_cards.rs b/src/pci_cards.rs index 92b0c3325..ddaf36c8a 100644 --- a/src/pci_cards.rs +++ b/src/pci_cards.rs @@ -53,6 +53,7 @@ pub trait PciCard: Send + Sync { pub type BuildCard = fn(&Options, u8) -> Result, String>; /// One kind of card. +#[derive(Debug)] pub struct CardDef { /// The name a `[[pci]]` entry gives as `card`. pub name: &'static str, From 201bb8b6e3c319a8a68d38186d64ba7f1e8b323e Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:09:47 -0700 Subject: [PATCH 08/12] config: add [[pci]] and --pci for fitting add-in cards Let a configuration fit a card in an add-in slot: one `[[pci]]` table per card with `slot`, `card` and the card's own options inline, or `--pci [BUS:]SLOT=CARD[,KEY=VALUE]...` on the command line. Validation runs with the rest of the configuration, so a mistake stops the run before any machine is built. The card's SCSI buses become controllers after the machine's own, so `[scsi.N]` can put a disk on them: controllers 2 and 3 for the first ISP12160 on an O2. Their ids go to 15, since the card's buses are wide; the O2's own buses stay at 7. Without a card the controller limits and their message are as before. --- src/config.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1ddc8fce1..e22e44043 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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]; @@ -1819,6 +1821,13 @@ pub struct MachineConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub port_forward: Vec, + /// 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, + /// VINO video-in configuration (IndyCam emulation source). #[serde(default)] pub vino: VinoConfig, @@ -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, @@ -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 = 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)); } @@ -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!( @@ -2730,6 +2746,13 @@ pub struct Cli { #[arg(long = "cache", value_name = "MODEL")] pub cache: Option, + /// 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, + /// 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, @@ -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; } @@ -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")); + } + +} From d7b632f8822496ba1e16a3b148906cf756e2ed07 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:14:50 -0700 Subject: [PATCH 09/12] ip32: fit add-in cards in the O2's PCI slot Wire configured cards into the O2. The bridge routes the slot's INTA to CRIME input 10 and INTB to INTD to the shared inputs 13 to 15, as the MACE specification's PCI interrupt tables have them, and Linux, OpenBSD and NetBSD agree; a shared input is the OR of every pin routed to it. Whether the board's connector wires INTB to INTD is not known. This is the only change a machine without a card sees, and nothing answers in the slot then. The machine builds the cards before it attaches `[scsi.N]` disks, so a disk can sit on a card's bus (controllers 2 and 3 for an ISP12160), then hands them to the bus, which attaches them and owns their lifetime: they start and stop with the onboard controllers, their disks are flushed and synced with the others, their copy-on-write overlays are saved by controller number, and their state is part of the bus's saved state. Restoring state saved with a different number of cards is refused. --- src/dev/mace_pci.rs | 78 ++++++++++++++++++++++++++++++++++++++++++--- src/ip32.rs | 39 ++++++++++++++++++++++- src/ip32/tests.rs | 63 ++++++++++++++++++++++++++++++++++++ src/ip32_machine.rs | 35 ++++++++++++++------ src/pci_addin.rs | 30 ++++++++++++++++- 5 files changed, 229 insertions(+), 16 deletions(-) diff --git a/src/dev/mace_pci.rs b/src/dev/mace_pci.rs index 6539b11de..227bbe03e 100644 --- a/src/dev/mace_pci.rs +++ b/src/dev/mace_pci.rs @@ -33,13 +33,19 @@ pub const MACEPCI_REVISION_VALUE: u32 = 1; pub const IP32_SCSI_SLOT: u8 = 1; pub const IP32_SCSI_SLOT_B: u8 = 2; -/// The one add-in slot: the PCI board position in the O2's tray. Linux and -/// OpenBSD both number it device 3 and take its INTA to CRIME input 10. +/// The one add-in slot: the PCI board position in the O2's tray, device 3 +/// (slot 0 in the MACE specification's tables). Its INTA is CRIME input 10 +/// and INTB to INTD the shared inputs 13 to 15; Linux, OpenBSD and NetBSD +/// route them the same way. pub const IP32_PCI_SLOT: u8 = 3; pub const IP32_PCI_CRIME_INT: u32 = 10; +/// CRIME's inputs for a slot's INTB, INTC and INTD. They are shared: any +/// device's pin can be routed to one, so each is the OR of its pins. +pub const IP32_PCI_CRIME_SHARED: u32 = 13; /// The O2's PCI bus, for the configuration and the `pci slots` command. The -/// board's INTB to INTD wiring is not known, so those pins are left unrouted. +/// slot's four pins are routed as the MACE specification has them; what +/// the board's connector wires INTB to INTD to is not known. pub const BUSES: &[BusDesc] = &[BusDesc { id: "pci", title: "the O2's PCI bus", @@ -50,7 +56,7 @@ pub const BUSES: &[BusDesc] = &[BusDesc { device: IP32_PCI_SLOT, label: "", evidence: Evidence::Established, - source: "Linux arch/mips/pci/fixup-ip32.c (irq_tab_mace); OpenBSD sys/arch/sgi/pci/macepcibridge.c (mace_pcibr_intr_map); O2 Workstation Hardware Reference Guide 007-3275-003, installing a PCI board", + source: "MACE specification, PCI interrupt input tables (Tables 85 and 87, pp. 164 and 166; Table 90, p. 179), which put device 3 (slot 0) on CRIME inputs 10, 13, 14 and 15; Linux arch/mips/pci/fixup-ip32.c (irq_tab_mace), OpenBSD macepcibridge.c (mace_pcibr_intr_map) and NetBSD macepci_intr_map agree; the board connector's wiring of INTB to INTD is unknown; O2 Workstation Hardware Reference Guide 007-3275-003, installing a PCI board", settled_by: "", }), ], @@ -68,6 +74,10 @@ fn crime_input(slot: u8, pin: u8) -> Option { match (slot, pin) { (IP32_SCSI_SLOT, 1) => Some(IP32_SCSI_CRIME_INT), (IP32_SCSI_SLOT_B, 1) => Some(IP32_SCSI_CRIME_INT_B), + (IP32_PCI_SLOT, 1) => Some(IP32_PCI_CRIME_INT), + // INTB to INTD, on the shared inputs 13 to 15, where the kernels + // look for them. Whether the board's connector wires them is unknown. + (IP32_PCI_SLOT, pin @ 2..=4) => Some(IP32_PCI_CRIME_SHARED + pin as u32 - 2), _ => None, } } @@ -903,7 +913,7 @@ mod tests { let (a, b, c) = (fake(PCI_INTA), fake(PCI_INTA), fake(PCI_INTA + 1)); pci.attach(IP32_SCSI_SLOT, "a", a.clone()); pci.attach(IP32_SCSI_SLOT_B, "b", b.clone()); - pci.attach(3, "c", c.clone()); + pci.attach(7, "c", c.clone()); a.irq.get().unwrap().set(true); b.irq.get().unwrap().set(true); c.irq.get().unwrap().set(true); @@ -931,4 +941,62 @@ mod tests { assert!(report[1].contains("device 1: onboard"), "{report:?}"); assert!(report[3].contains("device 3: add-in slot (established), empty"), "{report:?}"); } + + fn pin_fake(pin: u8) -> Arc { + Arc::new(Fake { + cfg: PciConfig::new(0x1077, 0x1216, 0x01_0000, 1, [Bar::Unused; 6], pin), + last: Mutex::new(None), + irq: std::sync::OnceLock::new(), + }) + } + + /// Each pin of a card in the add-in slot reaches its own CRIME input: + /// INTA 10, INTB to INTD the shared 13 to 15. + #[test] + fn each_pin_of_the_add_in_slot_reaches_its_crime_input() { + for (pin, input) in [(PCI_INTA, 10), (PCI_INTA + 1, 13), (PCI_INTA + 2, 14), (PCI_INTA + 3, 15)] { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime.clone(), test_ram()); + let card = pin_fake(pin); + pci.attach(IP32_PCI_SLOT, "card", card.clone()); + card.irq.get().unwrap().set(true); + assert_eq!(*crime.0.lock().unwrap(), vec![(input, true)], "pin {pin}"); + card.irq.get().unwrap().set(false); + assert_eq!(crime.0.lock().unwrap().last(), Some(&(input, false)), "pin {pin}"); + } + assert_eq!(IP32_PCI_CRIME_INT, 10); + assert_eq!(IP32_PCI_CRIME_SHARED, 13); + } + + /// A shared input stays up until every pin routed to it has dropped: + /// the slot's INTB and another device's pin on CRIME 13. + #[test] + fn two_pins_on_a_shared_crime_input_or_together() { + fn route(slot: u8, pin: u8) -> Option { + crime_input(slot, pin).or((slot == 5 && pin == 1).then_some(IP32_PCI_CRIME_SHARED)) + } + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::with_routing(crime.clone(), route, test_ram()); + let (slot, other) = (pin_fake(PCI_INTA + 1), pin_fake(PCI_INTA)); + pci.attach(IP32_PCI_SLOT, "slot", slot.clone()); + pci.attach(5, "other", other.clone()); + let last = || *crime.0.lock().unwrap().last().unwrap(); + slot.irq.get().unwrap().set(true); + assert_eq!(last(), (13, true)); + other.irq.get().unwrap().set(true); + slot.irq.get().unwrap().set(false); + assert_eq!(last(), (13, true), "the other device's pin still holds it"); + other.irq.get().unwrap().set(false); + assert_eq!(last(), (13, false)); + } + + #[test] + fn pci_slots_shows_a_fitted_cards_routing() { + let crime = Arc::new(Crime(Mutex::new(Vec::new()))); + let pci = MacePci::new(crime, test_ram()); + pci.attach(IP32_PCI_SLOT, "card", pin_fake(PCI_INTA)); + let report = pci.slots_report(); + assert!(report[3].contains("holds card 1077:1216"), "{report:?}"); + assert!(report[3].contains("INTA CRIME 10, INTB CRIME 13, INTC CRIME 14, INTD CRIME 15"), "{report:?}"); + } } diff --git a/src/ip32.rs b/src/ip32.rs index 1069ae454..4247440e5 100644 --- a/src/ip32.rs +++ b/src/ip32.rs @@ -175,6 +175,8 @@ pub struct Ip32Options { pub keyboard: Option, /// The Ethernet address in the 1-Wire ID chip, if not the default. pub mac: Option<[u8; 6]>, + /// Cards fitted in the add-in slot, with their disks attached. + pub cards: Vec, } /// Whether the per-register access counters (MACE's, GBE's, the rendering @@ -430,13 +432,25 @@ pub struct Ip32Bus { /// The SCSI controller, when one is attached, so the trace can report /// what the driver asked it for. pub scsi: Mutex>>, + /// The cards in the add-in slot. Fixed at power-on: a card is fitted + /// with the machine, not hot-plugged. + pub cards: Vec, } impl Ip32Bus { /// Flush the disks on every SCSI controller. pub fn flush_disks(&self) -> std::io::Result<()> { let controllers = self.scsi.lock().unwrap().clone(); - controllers.iter().try_for_each(|c| c.bus().flush()) + controllers.iter().try_for_each(|c| c.bus().flush())?; + self.cards.iter().flat_map(|f| f.buses()).try_for_each(|(_, b)| b.flush()) + } + + /// Every SCSI bus by controller number: the two onboard controllers' + /// first, then each card's. + pub fn scsi_buses<'a>(&'a self, onboard: &'a [Arc]) -> Vec<(usize, &'a crate::scsi::ScsiBus)> { + let mut all: Vec<_> = onboard.iter().enumerate().map(|(i, c)| (i, c.bus())).collect(); + all.extend(self.cards.iter().flat_map(|f| f.buses()).map(|(n, b)| (n as usize, b))); + all } /// `ram_bytes` of memory in the usual fitting for that size, or, for a @@ -474,6 +488,7 @@ impl Ip32Bus { mace: Mace::new(ram.clone(), crime, ust), misses: Arc::new(Misses::default()), scsi: Mutex::new(Vec::new()), + cards: opts.cards, }; bus.gbe.tr.start_from_env(); if let Some(mac) = opts.mac { @@ -504,6 +519,12 @@ impl Ip32Bus { list.push(scsi); list.push(scsi_b); } + // Cards in the add-in slot, on the same bridge the controllers are. + for f in &bus.cards { + f.card.set_heartbeat(bus.mace.id.heartbeat.clone()); + f.card.register_locks(); + bus.mace.pci.attach(f.device, f.card.name(), f.card.function()); + } // A saved flash carries the guest's environment -- boot paths, console // choice, everything the PROM's `setenv` writes -- from one run to // the next, which is what makes an install worth doing once. @@ -667,6 +688,9 @@ impl crate::traits::Saveable for Ip32Bus { t.insert("mace".into(), self.mace.save_state()); let scsi = self.scsi.lock().unwrap().iter().map(|c| c.save_state()).collect(); t.insert("scsi".into(), toml::Value::Array(scsi)); + if !self.cards.is_empty() { + t.insert("cards".into(), toml::Value::Array(self.cards.iter().map(|f| f.card.save_state()).collect())); + } if let Some(words) = self.ram.stack_page_words() { t.insert("stack_page".into(), toml::Value::Array(words.into_iter().map(crate::snapshot::hex_u64).collect())); } @@ -696,6 +720,16 @@ impl crate::traits::Saveable for Ip32Bus { c.load_state(s)?; } } + let saved_cards = match get_field(v, "cards") { + Some(toml::Value::Array(a)) => a.as_slice(), + _ => &[], + }; + if saved_cards.len() != self.cards.len() { + return Err(format!("saved state has {} PCI cards, this machine {}", saved_cards.len(), self.cards.len())); + } + for (f, s) in self.cards.iter().zip(saved_cards) { + f.card.load_state(s)?; + } if let Some(toml::Value::Array(a)) = get_field(v, "stack_page") { let words: Option> = a.iter().map(crate::snapshot::toml_u64).collect(); self.ram.restore_stack_page(&words.ok_or("stack_page: not a list of dwords")?)?; @@ -718,6 +752,9 @@ impl Ip32Bus { for d in devices { d.power_on(); } + for f in &self.cards { + f.card.power_on(); + } } /// RAM and the flash, as `ram.bin` and `flash.bin` in `dir`. diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 88b725934..927d284fc 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -958,3 +958,66 @@ fn every_device_answers_the_monitor_through_the_bus() { } assert!(bus.execute_command("nonesuch", &[], Box::new(std::io::sink())).is_err()); } + +/// A configuration read of `reg` in `device`'s header on bus 0. +fn pci_config_read(bus: &Ip32Bus, device: u32, reg: u32) -> u32 { + use crate::traits::BusDevice; + let pci = &bus.mace.pci; + pci.write32((MACEPCI_BASE + macepci_reg::CONFIG_ADDR) as u64, 1 << 31 | device << 11 | reg << 2); + pci.read32((MACEPCI_BASE + macepci_reg::CONFIG_DATA) as u64).data +} + +fn o2_with_cards(entries: &[&str]) -> Arc { + let entries: Vec<_> = entries.iter().map(|e| crate::pci_addin::PciEntry::parse_cli(e).unwrap()).collect(); + let placed = crate::pci_addin::validate_pci(crate::config::MachineProfile::O2Ip32, &entries).unwrap(); + let opts = Ip32Options { cards: crate::pci_addin::build_cards(placed).unwrap(), ..Default::default() }; + Ip32Bus::with_options(O2Memory::single(1 << 20), vec![0u8; PROM_SIZE as usize], opts) +} + +#[test] +fn a_card_in_the_add_in_slot_enumerates_at_device_3_and_the_slot_is_empty_without_one() { + let empty = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + assert_eq!(pci_config_read(&empty, 3, 0), 0xffff_ffff, "nothing answers in the slot"); + let bus = o2_with_cards(&["3=isp12160"]); + assert_eq!(pci_config_read(&bus, 3, 0), 0x1216_1077, "QLogic ISP12160 at device 3"); + assert_eq!(pci_config_read(&bus, 0, 0), 0xffff_ffff, "device 0 is still empty"); + assert_eq!(pci_config_read(&bus, 4, 0), 0xffff_ffff, "and so is device 4"); + let report = bus.mace.pci.slots_report(); + assert!(report[3].contains("holds isp12160 1077:1216"), "{report:?}"); + assert_eq!(bus.scsi_buses(&[]).iter().map(|&(n, _)| n).collect::>(), vec![2, 3], "after the two onboard controllers"); +} + +#[test] +fn each_pin_of_a_card_sets_its_crime_input_and_no_other() { + use crate::pci::{Bar, PciBusDevice, PciConfig, PCI_INTA}; + struct Pin(PciConfig, std::sync::OnceLock); + impl PciBusDevice for Pin { + fn config(&self) -> &PciConfig { &self.0 } + fn bar_read(&self, _: usize, _: u32, _: usize) -> u32 { 0 } + fn bar_write(&self, _: usize, _: u32, _: u32, _: usize) {} + fn connect_interrupt(&self, line: crate::irq::IrqLine) { let _ = self.1.set(line); } + } + for (pin, input) in [(PCI_INTA, 10), (PCI_INTA + 1, 13), (PCI_INTA + 2, 14), (PCI_INTA + 3, 15)] { + let bus = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + let card = Arc::new(Pin(PciConfig::new(0x1077, 0x1216, 0x01_0000, 1, [Bar::Unused; 6], pin), Default::default())); + bus.mace.pci.attach(IP32_PCI_SLOT, "pin", card.clone()); + let before = bus.crime.regs.intstat(); + card.1.get().expect("wired at attach").set(true); + assert_eq!(bus.crime.regs.intstat() & !before, 1 << input, "pin {pin}: CRIME bit {input}, nothing else"); + card.1.get().unwrap().set(false); + assert_eq!(bus.crime.regs.intstat(), before, "pin {pin}"); + } +} + +#[test] +fn a_card_changes_the_saved_state_and_a_machine_without_it_refuses_it() { + use crate::traits::Saveable; + let with = o2_with_cards(&["3=isp12160"]); + let without = Ip32Bus::new(1 << 20, vec![0u8; PROM_SIZE as usize]); + let saved = with.save_state(); + with.load_state(&saved).expect("the same machine loads its own state"); + let err = without.load_state(&saved).unwrap_err(); + assert!(err.contains("slot 3, which is empty here"), "{err}"); + let err = with.load_state(&without.save_state()).unwrap_err(); + assert!(err.contains("0 PCI cards, this machine 1"), "{err}"); +} diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index a7a42a6d4..dcc81aee0 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -180,11 +180,18 @@ impl O2Machine { // machines; `controller` is the bus (0 internal, 1 external). The // buses are filled before the controllers exist. let scsi = [crate::dev::aic7880::Aic7880::new_bus(), crate::dev::aic7880::Aic7880::new_bus()]; + // Cards in the add-in slot bring buses of their own, numbered after + // the two onboard ones. + let cards = crate::pci_addin::build_cards(crate::pci_addin::validate_pci(cfg.machine.profile, &cfg.pci)?)?; let attached = crate::scsi::attach_from_config(cfg, Some("ip32"), &|c| { - scsi.get(c as usize) - .ok_or_else(|| format!("controller {c} (an O2 has controllers 0 and 1)")) + if let Some(bus) = scsi.get(c as usize) { + return Ok(bus); + } + cards.iter().flat_map(|f| f.buses()).find(|&(n, _)| n == c).map(|(_, b)| b) + .ok_or_else(|| format!("controller {c} (an O2 has controllers 0 and 1, and each add-in card its own)")) })?; let opts = Ip32Options { + cards, scsi: Some(scsi), flash_file: Some(cfg.nvram.clone().into()), keyboard: (!cfg.headless).then_some(o2.mouse), @@ -542,6 +549,9 @@ impl O2Machine { for ctl in self.bus.scsi.lock().unwrap().iter() { ctl.stop(); } + for f in &self.bus.cards { + f.card.stop(); + } if !self.bus.crime.re.drain(Duration::from_secs(2)) { eprintln!("iris: O2 stopped with rendering engine writes still queued"); } @@ -569,6 +579,9 @@ impl O2Machine { for ctl in self.bus.scsi.lock().unwrap().iter() { ctl.start(); } + for f in &self.bus.cards { + f.card.start(); + } } /// The hardware as at power-on, with the CPU stopped: what a CRIME cold @@ -619,10 +632,11 @@ impl O2Machine { chunks.bank_chunks[0] = put_bank_words(&store, &self.bus.ram) .map_err(|e| format!("CAS memory put: {e}"))?; snap.write_chunks_manifest(&chunks).map_err(|e| e.to_string())?; - for (i, ctl) in self.bus.scsi.lock().unwrap().iter().enumerate() { + let onboard = self.bus.scsi.lock().unwrap().clone(); + for (i, bus) in self.bus.scsi_buses(&onboard) { let dir = snap.dir.join(format!("ctrl{i}")); std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - let overlays = ctl.export_overlays(&dir).map_err(|e| format!("COW overlay export (controller {i}): {e}"))?; + let overlays = bus.export_overlays(&dir).map_err(|e| format!("COW overlay export (controller {i}): {e}"))?; snap.write_toml(&format!("cow{i}.toml"), &crate::machine::build_cow_table(overlays)) .map_err(|e| e.to_string())?; } @@ -675,7 +689,8 @@ impl O2Machine { return Err(format!("snapshot memory is {} MB, this O2 has {} MB", memory.len() >> 18, self.bus.ram.len() >> 20)); } - let ctrls = self.bus.scsi.lock().unwrap().len(); + let onboard = self.bus.scsi.lock().unwrap().clone(); + let ctrls = self.bus.scsi_buses(&onboard).iter().map(|&(i, _)| i + 1).max().unwrap_or(0); let overlays = (0..ctrls) .map(|i| snap.read_toml(&format!("cow{i}.toml")).ok().map(|t| crate::machine::parse_cow_table(&t))) .collect(); @@ -701,9 +716,10 @@ impl O2Machine { self.boot.load(&s.boot); self.bus.set_flash(&s.flash)?; self.bus.ram.restore_words(&s.memory)?; - for (i, ctl) in self.bus.scsi.lock().unwrap().iter().enumerate() { + let onboard = self.bus.scsi.lock().unwrap().clone(); + for (i, bus) in self.bus.scsi_buses(&onboard) { if let Some(Some(sets)) = s.overlays.get(i) { - ctl.import_overlays(&s.dir.join(format!("ctrl{i}")), sets) + bus.import_overlays(&s.dir.join(format!("ctrl{i}")), sets) .map_err(|e| format!("COW overlay import (controller {i}): {e}"))?; } } @@ -814,7 +830,8 @@ impl Workstation for O2Machine { } fn pending_chd_sync_count(&self) -> usize { - self.bus.scsi.lock().unwrap().iter().map(|c| c.bus().pending_chd_sync_count()).sum() + let onboard = self.bus.scsi.lock().unwrap().clone(); + self.bus.scsi_buses(&onboard).iter().map(|(_, b)| b.pending_chd_sync_count()).sum() } fn flush_disks(&self) -> std::io::Result<()> { self.bus.flush_disks() @@ -826,7 +843,7 @@ impl Workstation for O2Machine { cancel: &dyn Fn() -> bool, ) -> std::io::Result { let controllers = self.bus.scsi.lock().unwrap().clone(); - let buses: Vec<_> = controllers.iter().map(|c| c.bus()).collect(); + let buses: Vec<_> = self.bus.scsi_buses(&controllers).into_iter().map(|(_, b)| b).collect(); crate::scsi::ScsiBus::sync_chd_disks(&buses, only, progress, cancel) } diff --git a/src/pci_addin.rs b/src/pci_addin.rs index 531d3f3fe..b42f5df47 100644 --- a/src/pci_addin.rs +++ b/src/pci_addin.rs @@ -14,7 +14,8 @@ use serde::{Deserialize, Serialize}; use crate::config::MachineProfile; -use crate::pci_cards::{card_def, card_names, CardDef, Options}; +use crate::pci_cards::{card_def, card_names, CardDef, Options, PciCard}; +use crate::scsi::ScsiBus; /// How well a fact is known, from strongest to weakest. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -202,6 +203,33 @@ pub struct Placement { pub scsi_base: u8, } +/// A card built for its slot. +pub struct Fitted { + pub bus: &'static str, + pub device: u8, + /// The controller number of the card's first SCSI bus. + pub scsi_base: u8, + pub card: Box, +} + +/// Build the card of every placement. +pub fn build_cards(placements: Vec) -> Result, String> { + placements + .into_iter() + .map(|p| { + let card = (p.card.build)(&p.options, p.scsi_base).map_err(|e| format!("card \"{}\" in {} slot {}: {e}", p.card.name, p.bus.id, p.slot.device))?; + Ok(Fitted { bus: p.bus.id, device: p.slot.device, scsi_base: p.scsi_base, card }) + }) + .collect() +} + +impl Fitted { + /// The card's buses with their controller numbers. + pub fn buses(&self) -> impl Iterator { + self.card.scsi_buses().into_iter().enumerate().map(|(i, b)| (self.scsi_base + i as u8, b)) + } +} + /// The bus an entry names, or the machine's only one. fn find_bus<'a>(all: &'a [BusDesc], noun: &str, bus: Option<&str>) -> Result<&'a BusDesc, String> { let ids = || all.iter().map(|b| b.id).collect::>().join(", "); From 4afadcb863e353abb1b9973d08ea103b29c4ca79 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:16:16 -0700 Subject: [PATCH 10/12] snapshot: record the fitted PCI cards and refuse a mismatch A snapshot taken with a card in the slot holds the card's registers and its ring state. Restored into a machine without the card, that state has nowhere to go; restored the other way round, the card would sit at power-on under a kernel that has been driving it. Neither is a state the machine was ever in, so list the fitted cards in the manifest, by bus, device number and card, and compare before touching anything. The O2 refuses with a line per difference. Device numbers, not chassis labels, are recorded, so relabelling a slot later cannot break an old save. The key is optional and left out when nothing is fitted, so snapshots without cards read and write as before and the schema version does not change; an older build restoring a save with cards ignores the key and fails later on the saved slot instead. The check does not honour IRIS_SNAPSHOT_SKIP_CHECK. --- src/exit_persistence_tests.rs | 32 ++++++++++ src/ip32_machine.rs | 15 ++++- src/pci_addin.rs | 10 ++- src/snapshot.rs | 116 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 4 deletions(-) diff --git a/src/exit_persistence_tests.rs b/src/exit_persistence_tests.rs index f2d507cb1..3b585cd3c 100644 --- a/src/exit_persistence_tests.rs +++ b/src/exit_persistence_tests.rs @@ -678,6 +678,38 @@ fn o2_stop_flushes_its_disks() { }); } +/// A snapshot taken with other PCI cards is refused before anything else is +/// read. The manifest is the only file written, so any other refusal would +/// name something else. +#[test] +fn o2_restore_refuses_a_snapshot_taken_with_other_cards() { + use crate::snapshot::{Manifest, PciRef, Snapshot}; + let _serial = serial(); + on_big_stack(|| { + let dir = Dir::new("o2-cards"); + make_images(&dir); + let mut cfg = config(Machine::O2, &dir, false); + cfg.pci.push(crate::pci_addin::PciEntry::parse_cli("3=isp12160").unwrap()); + let mut m = build(cfg); + let o2 = m.as_any_mut().downcast_mut::().unwrap(); + let snap = Snapshot::new(dir.file("snap")); + snap.ensure_dir().unwrap(); + let mut manifest = Manifest::for_current_save(); + manifest.machine = Some("o2_ip32".into()); + snap.write_manifest(&manifest).unwrap(); + let err = o2.read_snapshot_in(Snapshot::new(dir.file("snap")), "snap").err().expect("refused"); + assert!(err.contains("snapshot PCI cards differ"), "{err}"); + assert!(err.contains("this config has pci bus \"pci\" slot 3 = isp12160, the snapshot has no card there"), "{err}"); + + // The same card in the same slot passes this check; the restore then + // stops at the first file this test did not write. + manifest.pci = vec![PciRef { bus: "pci".into(), slot: 3, card: "isp12160".into(), options: String::new() }]; + snap.write_manifest(&manifest).unwrap(); + let err = o2.read_snapshot_in(Snapshot::new(dir.file("snap")), "snap").err().expect("no state files"); + assert!(!err.contains("PCI cards"), "{err}"); + }); +} + /// The O2 has a second way to switch off, for a machine nothing is /// listening to: its service loop flushes the buses and exits itself. #[test] diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index dcc81aee0..75a64de95 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -621,6 +621,7 @@ impl O2Machine { manifest.nvram = self.flash_path.clone(); manifest.cpu_model = Some(self.cpu.model_name().to_string()); manifest.cpu_cache = Some(self.cpu.cache_kind().to_string()); + manifest.pci = self.bus.cards.iter().map(|f| f.pci_ref()).collect(); snap.write_manifest(&manifest).map_err(|e| e.to_string())?; let sv = manifest.schema_version; snap.write_state("cpu", &self.cpu.save_state(), sv).map_err(|e| e.to_string())?; @@ -659,9 +660,13 @@ impl O2Machine { } fn read_snapshot(&self, name: &str) -> Result { + self.read_snapshot_in(crate::snapshot::Snapshot::new(std::path::PathBuf::from("saves").join(name)), name) + } + + /// [`Self::read_snapshot`] for the snapshot in `snap`'s directory. + pub(crate) fn read_snapshot_in(&self, snap: crate::snapshot::Snapshot, name: &str) -> Result { use crate::chunk_store::{get_chunks_as_words, ChunkStore}; - use crate::snapshot::{Snapshot, SCHEMA_VERSION}; - let snap = Snapshot::new(std::path::PathBuf::from("saves").join(name)); + use crate::snapshot::SCHEMA_VERSION; let m = snap.read_manifest()?.ok_or_else(|| format!("saves/{name}: no snapshot.toml"))?; if m.machine.as_deref() != Some(SNAPSHOT_MACHINE) { return Err(format!("snapshot {name} is not of an O2")); @@ -678,6 +683,10 @@ impl O2Machine { m.schema_version )); } + // Before anything else is compared: a snapshot of a machine with + // other cards is a different machine. + let fitted: Vec<_> = self.bus.cards.iter().map(|f| f.pci_ref()).collect(); + crate::snapshot::check_pci(&m.pci, &fitted)?; // As the Indy: build, CPU and disks must match. crate::snapshot::check_provenance(&m, self.cpu.model_name(), self.cpu.cache_kind(), &self.disks)?; let sv = m.schema_version; @@ -754,7 +763,7 @@ impl O2Machine { } /// A snapshot read and checked, ready to put into the machine. -struct O2Snapshot { +pub(crate) struct O2Snapshot { cpu: toml::Value, bus: toml::Value, boot: toml::Value, diff --git a/src/pci_addin.rs b/src/pci_addin.rs index b42f5df47..05fc3efc6 100644 --- a/src/pci_addin.rs +++ b/src/pci_addin.rs @@ -209,6 +209,8 @@ pub struct Fitted { pub device: u8, /// The controller number of the card's first SCSI bus. pub scsi_base: u8, + /// The entry's options as `key=value,...`, sorted, for a snapshot. + pub options: String, pub card: Box, } @@ -218,12 +220,18 @@ pub fn build_cards(placements: Vec) -> Result, String> { .into_iter() .map(|p| { let card = (p.card.build)(&p.options, p.scsi_base).map_err(|e| format!("card \"{}\" in {} slot {}: {e}", p.card.name, p.bus.id, p.slot.device))?; - Ok(Fitted { bus: p.bus.id, device: p.slot.device, scsi_base: p.scsi_base, card }) + let options = p.options.iter().map(|(k, v)| format!("{k}={}", v.as_str().map_or_else(|| v.to_string(), String::from))).collect::>().join(","); + Ok(Fitted { bus: p.bus.id, device: p.slot.device, scsi_base: p.scsi_base, options, card }) }) .collect() } impl Fitted { + /// Where the card is and what it is, as a snapshot records it. + pub fn pci_ref(&self) -> crate::snapshot::PciRef { + crate::snapshot::PciRef { bus: self.bus.into(), slot: self.device, card: self.card.name().into(), options: self.options.clone() } + } + /// The card's buses with their controller numbers. pub fn buses(&self) -> impl Iterator { self.card.scsi_buses().into_iter().enumerate().map(|(i, b)| (self.scsi_base + i as u8, b)) diff --git a/src/snapshot.rs b/src/snapshot.rs index f38835381..6ae400b3c 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -70,6 +70,50 @@ pub struct DiskRef { pub size_bytes: u64, } +/// A PCI add-in card fitted when a snapshot was taken, by where it was. +/// `slot` is the PCI device number, never a chassis label, so a snapshot +/// outlives a relabelling. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PciRef { + pub bus: String, + pub slot: u8, + pub card: String, + /// The card's options as `key=value,...`, sorted. + pub options: String, +} + +impl PciRef { + fn describe(&self) -> String { + let options = if self.options.is_empty() { String::new() } else { format!(" ({})", self.options) }; + format!("pci bus \"{}\" slot {} = {}{options}", self.bus, self.slot, self.card) + } +} + +/// Refuse a restore into a machine whose fitted cards are not the ones the +/// snapshot was taken with. A card's state restored without the card, or a +/// card left at power-on under a running kernel, is not a state anything +/// was ever in. Not overridable: there is nothing to restore it into. +pub fn check_pci(snapshot: &[PciRef], current: &[PciRef]) -> Result<(), String> { + let mut problems = Vec::new(); + for s in snapshot { + match current.iter().find(|c| c.bus == s.bus && c.slot == s.slot) { + None => problems.push(format!("snapshot has {}, this config has no card there", s.describe())), + Some(c) if c != s => problems.push(format!("snapshot has {}, this config has {}", s.describe(), c.describe())), + Some(_) => {} + } + } + for c in current { + if !snapshot.iter().any(|s| s.bus == c.bus && s.slot == c.slot) { + problems.push(format!("this config has {}, the snapshot has no card there", c.describe())); + } + } + if problems.is_empty() { + Ok(()) + } else { + Err(format!("snapshot PCI cards differ:\n - {}", problems.join("\n - "))) + } +} + /// Reject a restore whose CPU model differs from the running machine's. /// `None` from the snapshot means a legacy manifest — the model was a build /// flag then, so there is nothing to compare and the restore is allowed. @@ -192,6 +236,9 @@ pub struct Manifest { /// for the O2. None for the Indy and Indigo2 and every older manifest. A /// snapshot loads only into the machine it names. pub machine: Option, + /// The PCI add-in cards fitted at capture time. Empty when none were, + /// and for every manifest written before cards could be fitted. + pub pci: Vec, } impl Manifest { @@ -218,6 +265,7 @@ impl Manifest { cpu_model: None, cpu_cache: None, machine: None, + pci: Vec::new(), } } @@ -266,6 +314,17 @@ impl Manifest { if let Some(m) = &self.machine { tbl.insert("machine".into(), Value::String(m.clone())); } + if !self.pci.is_empty() { + let cards: Vec = self.pci.iter().map(|c| { + let mut t = toml::map::Map::new(); + t.insert("bus".into(), Value::String(c.bus.clone())); + t.insert("slot".into(), Value::Integer(c.slot as i64)); + t.insert("card".into(), Value::String(c.card.clone())); + t.insert("options".into(), Value::String(c.options.clone())); + Value::Table(t) + }).collect(); + tbl.insert("pci".into(), Value::Array(cards)); + } Value::Table(tbl) } @@ -310,6 +369,22 @@ impl Manifest { let cpu_model = tbl.get("cpu_model").and_then(|x| x.as_str()).map(String::from); let cpu_cache = tbl.get("cpu_cache").and_then(|x| x.as_str()).map(String::from); let machine = tbl.get("machine").and_then(|x| x.as_str()).map(String::from); + // Cards: absent in a manifest from before they existed, which had + // none. A card entry that does not parse is an error, not a + // card-less snapshot, since the restore would then run without it. + let pci = match tbl.get("pci") { + None => Vec::new(), + Some(v) => v.as_array().ok_or("manifest: pci is not a list")?.iter().map(|x| { + let t = x.as_table().ok_or("manifest: a pci entry is not a table")?; + let text = |k: &str| t.get(k).and_then(|v| v.as_str()).map(String::from).ok_or(format!("manifest: pci entry has no {k}")); + Ok(PciRef { + bus: text("bus")?, + slot: t.get("slot").and_then(|v| v.as_integer()).and_then(|n| u8::try_from(n).ok()).ok_or("manifest: pci entry has no slot")?, + card: text("card")?, + options: t.get("options").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + }) + }).collect::, String>>()?, + }; Ok(Self { schema_version, iris_git_rev, @@ -324,6 +399,7 @@ impl Manifest { cpu_model, cpu_cache, machine, + pci, }) } } @@ -782,6 +858,7 @@ mod tests { cpu_model: Some("R5000".into()), cpu_cache: Some("shadow".into()), machine: Some("o2_ip32".into()), + pci: vec![PciRef { bus: "pci".into(), slot: 3, card: "isp12160".into(), options: String::new() }], }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -797,6 +874,7 @@ mod tests { assert_eq!(m2.nvram, m.nvram); assert_eq!(m2.cpu_model, m.cpu_model); assert_eq!(m2.machine, m.machine); + assert_eq!(m2.pci, m.pci); } #[test] @@ -815,6 +893,7 @@ mod tests { cpu_model: None, cpu_cache: None, machine: None, + pci: vec![], }; let v = m.to_toml(); let m2 = Manifest::from_toml(&v).expect("parse"); @@ -827,6 +906,43 @@ mod tests { assert!(m2.nvram.is_none()); assert!(m2.cpu_model.is_none(), "legacy manifest has no cpu_model"); assert!(m2.machine.is_none(), "an Indy/Indigo2 manifest names no machine"); + assert!(m2.pci.is_empty(), "a manifest without cards names none, and writes no key"); + assert!(v.get("pci").is_none()); + } + + fn card(bus: &str, slot: u8, name: &str) -> PciRef { + PciRef { bus: bus.into(), slot, card: name.into(), options: String::new() } + } + + #[test] + fn a_restore_needs_the_same_cards_in_the_same_slots() { + let fitted = [card("pci", 3, "isp12160")]; + check_pci(&fitted, &fitted).expect("same card, same slot"); + check_pci(&[], &[]).expect("no cards on either side"); + assert_eq!( + check_pci(&fitted, &[]).unwrap_err(), + "snapshot PCI cards differ:\n - snapshot has pci bus \"pci\" slot 3 = isp12160, this config has no card there" + ); + assert_eq!( + check_pci(&[], &fitted).unwrap_err(), + "snapshot PCI cards differ:\n - this config has pci bus \"pci\" slot 3 = isp12160, the snapshot has no card there" + ); + let other = [card("pci", 3, "other")]; + assert!(check_pci(&fitted, &other).unwrap_err().contains("this config has pci bus \"pci\" slot 3 = other")); + let tuned = [PciRef { options: "depth=4".into(), ..card("pci", 3, "isp12160") }]; + assert!(check_pci(&fitted, &tuned).unwrap_err().contains("(depth=4)"), "options are part of the card"); + let two = [card("pci", 3, "isp12160"), card("pci", 4, "isp12160")]; + assert!(check_pci(&fitted, &two).unwrap_err().contains("slot 4")); + } + + #[test] + fn a_manifest_with_a_broken_card_entry_is_an_error() { + let mut m = Manifest::for_current_save(); + m.pci = vec![card("pci", 3, "isp12160")]; + let mut v = m.to_toml(); + assert_eq!(Manifest::from_toml(&v).unwrap().pci, m.pci); + v.as_table_mut().unwrap().insert("pci".into(), Value::Array(vec![Value::String("x".into())])); + assert!(Manifest::from_toml(&v).is_err(), "not a card-less snapshot"); } #[test] From 28888de2abc79bc77049150d80d39725671a96e9 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:31:36 -0700 Subject: [PATCH 11/12] pci_cards: add the AIC-7880 card The O2's IRIX has no driver for the ISP12160: it finds the card on the PCI bus and lists it as an unclaimed adapter, but has nothing to bind, so no controller or disk shows in `hinv`. The Adaptec AIC-7880 is the chip the O2 already drives onboard, and it also came on PCI cards, so the same driver claims one in the add-in slot and the guest exercises the slot end to end: configuration, BARs, DMA and the interrupt on CRIME input 10. The card is the existing controller model with one bus, numbered as the next controller after the machine's, which is why a card is now built knowing its first controller number. --- src/ip32/tests.rs | 8 +++++++ src/pci_addin.rs | 2 +- src/pci_cards.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 927d284fc..dc3e5b500 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -1021,3 +1021,11 @@ fn a_card_changes_the_saved_state_and_a_machine_without_it_refuses_it() { let err = with.load_state(&without.save_state()).unwrap_err(); assert!(err.contains("0 PCI cards, this machine 1"), "{err}"); } + +#[test] +fn an_aic7880_card_is_the_onboard_chip_in_the_add_in_slot_with_the_next_controller_number() { + let bus = o2_with_cards(&["3=aic7880"]); + assert_eq!(pci_config_read(&bus, 3, 0), 0x8078_9004, "Adaptec AIC-7880 at device 3"); + let onboard = bus.scsi.lock().unwrap().clone(); + assert_eq!(bus.scsi_buses(&onboard).iter().map(|&(n, _)| n).collect::>(), vec![2], "no onboard controllers here, one card bus"); +} diff --git a/src/pci_addin.rs b/src/pci_addin.rs index 05fc3efc6..36a746430 100644 --- a/src/pci_addin.rs +++ b/src/pci_addin.rs @@ -469,7 +469,7 @@ mod tests { o2(&["1=isp12160"]).err().unwrap(), "pci[0]: device 1 on the O2's PCI bus is the onboard internal SCSI controller (AIC-7880), not an add-in slot. Add-in slots: 3." ); - assert_eq!(o2(&["3=rad9"]).err().unwrap(), "pci[0]: unknown card \"rad9\" (cards: isp12160)."); + assert_eq!(o2(&["3=rad9"]).err().unwrap(), "pci[0]: unknown card \"rad9\" (cards: isp12160, aic7880)."); assert_eq!( o2(&["3=isp12160,volume=3"]).err().unwrap(), "pci[0] card \"isp12160\": unknown option \"volume\" (this card takes no options)." diff --git a/src/pci_cards.rs b/src/pci_cards.rs index ddaf36c8a..23fceed0c 100644 --- a/src/pci_cards.rs +++ b/src/pci_cards.rs @@ -10,6 +10,7 @@ use std::collections::BTreeMap; use std::sync::atomic::AtomicU64; use std::sync::Arc; +use crate::dev::aic7880::Aic7880; use crate::dev::isp12160::Isp12160; use crate::pci::PciBusDevice; use crate::scsi::ScsiBus; @@ -71,7 +72,14 @@ pub static CARDS: &[CardDef] = &[CardDef { options: &[], scsi_controllers: 2, build: |_, _| Ok(Box::new(IspCard(Arc::new(Isp12160::default())))), -}]; +}, CardDef { + name: "aic7880", + summary: "Adaptec AIC-7880, one Ultra SCSI bus on a PCI card", + options: &[], + scsi_controllers: 1, + build: |_, controller| Ok(Box::new(AhcCard(Aic7880::new_at(controller, Aic7880::new_bus())))), +}, +]; pub fn card_def(name: &str) -> Option<&'static CardDef> { CARDS.iter().find(|c| c.name == name) @@ -82,6 +90,44 @@ pub fn card_names() -> String { CARDS.iter().map(|c| c.name).collect::>().join(", ") } +/// The same chip as the O2's onboard controllers, on a card: the Adaptec +/// 2940UW and its kin carry it, and IRIX's driver for it binds by PCI id. +struct AhcCard(Arc); + +impl PciCard for AhcCard { + fn name(&self) -> &'static str { + "aic7880" + } + + fn function(&self) -> Arc { + self.0.clone() + } + + fn scsi_buses(&self) -> Vec<&ScsiBus> { + vec![self.0.bus()] + } + + fn set_heartbeat(&self, heartbeat: Arc) { + self.0.set_heartbeat(heartbeat); + } + + fn start(&self) { + Device::start(&*self.0); + } + + fn stop(&self) { + Device::stop(&*self.0); + } + + fn save_state(&self) -> toml::Value { + Saveable::save_state(&*self.0) + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + Saveable::load_state(&*self.0, v) + } +} + struct IspCard(Arc); impl PciCard for IspCard { @@ -140,6 +186,15 @@ mod tests { assert!(card_def("rad9").is_none()); } + #[test] + fn the_aic7880_brings_one_bus_and_an_adaptec_function() { + let def = card_def("aic7880").expect("registered"); + let card = (def.build)(&Options::new(), 2).unwrap(); + assert_eq!(card.scsi_buses().len(), def.scsi_controllers as usize); + let id = card.function().config().read(0); + assert_eq!((id & 0xffff, id >> 16), (0x9004, 0x8078), "Adaptec AIC-7880"); + } + #[test] fn two_cards_do_not_share_buses_or_state() { let def = card_def("isp12160").unwrap(); From bb21d46c025ab33d10fd342fd75c0fed24931012 Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 03:32:07 -0700 Subject: [PATCH 12/12] docs: document O2 PCI add-in cards Describe `[[pci]]`, `--pci`, the O2's one slot and its interrupt wiring, the two cards and how a card's SCSI buses are numbered, the `pci slots` monitor command and the snapshot rule. Say which card IRIX 6.5.30 drives: the AIC-7880 shows in `hinv` with its disk, the ISP12160 is found on the bus but has no driver. --- CHANGELOG.md | 20 ++++++++++++++++++++ FEATURES.md | 16 ++++++++++++++++ HELP.md | 10 ++++++++++ debug.md | 2 +- docs/o2-ip32.md | 18 +++++++++++++++++- 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10b95cec..98c44d12b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`, diff --git a/FEATURES.md b/FEATURES.md index 487c73b05..3690e3bc5 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -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[].` | 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 | @@ -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: | | `--monitor-port` | Monitor console port on 127.0.0.1 (default 8888). Give each iris running at once its own. | diff --git a/HELP.md b/HELP.md index 8b2494b9c..58f60e5f6 100644 --- a/HELP.md +++ b/HELP.md @@ -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" @@ -786,6 +795,7 @@ Configuration --nveeprom Indigo2 NVRAM EEPROM file (default: nveeprom.bin) --machine 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 newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12 --cpu r4400 (default), r5000, or r10000 --bank0..--bank3 RAM bank sizes (0/8/16/32/64/128; IP28: also 256/512) diff --git a/debug.md b/debug.md index 3f2dfb5fd..63ebcc836 100644 --- a/debug.md +++ b/debug.md @@ -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 ` | MACE PCI bridge / a function's configuration space | +| `pci status` / `pci slots` / `pci config ` | 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 | diff --git a/docs/o2-ip32.md b/docs/o2-ip32.md index a1ee2daf5..3072bd6a7 100644 --- a/docs/o2-ip32.md +++ b/docs/o2-ip32.md @@ -107,6 +107,22 @@ 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 @@ -114,7 +130,7 @@ The monitor's `save NAME` and `load NAME`, and iris-ci's `save` and 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.