From eb9efbbebc34ce647056e0a40a4df570cd0b4805 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:03:24 +0000 Subject: [PATCH 01/20] 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 ac725c33a..7637ea30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -327,6 +327,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 41fcf4fa9..96b267e06 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -1057,7 +1057,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 f81627099..679e23fe8 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]; @@ -436,6 +558,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] @@ -502,7 +632,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); @@ -529,6 +659,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 4209d07bece709c287ef6b971f8f593738bcd69e Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 02/20] 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 ef3611725..879ca4b5b 100644 --- a/HACKING.md +++ b/HACKING.md @@ -98,6 +98,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 f911743fd..03a64a2b4 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(); @@ -1618,6 +1620,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 @@ -1653,6 +2052,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 1ea58ab59caee05f4deb0d10916e88e88b5020ae Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 03/20] dp83843: add the Fuel Ethernet PHY Fuel fits a DP83843 at MII management address 1, where the Octane has an ICS1890. The IOC3 Ethernet controller needs the same packet and link machinery with either PHY, so keep that machinery shared: move the Octane's ICS1890 register model behind a PHY interface, and supply the DP83843's register and negotiation behavior through it. The MAC keeps its existing packet path. The interface carries the snapshot hooks too: the ICS1890 keeps the register save and load it had in the IOC3 model, and the DP83843 saves its registers and negotiation timers the same way, so the shared Ethernet block saves whichever PHY it holds. --- src/dev/dp83843.rs | 573 ++++++++++++++++++++++++++++++++++++++ src/dev/ics1890.rs | 423 ++++++++++++++++++++++++++++ src/dev/ioc3_eth.rs | 473 ++++--------------------------- src/dev/ioc3_eth/tests.rs | 36 +++ src/dev/mod.rs | 2 + 5 files changed, 1092 insertions(+), 415 deletions(-) create mode 100644 src/dev/dp83843.rs create mode 100644 src/dev/ics1890.rs diff --git a/src/dev/dp83843.rs b/src/dev/dp83843.rs new file mode 100644 index 000000000..8d3bb40c5 --- /dev/null +++ b/src/dev/dp83843.rs @@ -0,0 +1,573 @@ +//! The DP83843 MII model exposes the control, status and auto-negotiation +//! registers used by the Fuel IOC3 Ethernet port. A guest starts negotiation +//! through MII; the backend link level determines the simulated partner result. +//! +//! # What is approximated here +//! +//! Link settling and partner responses are software models. BMSR reports the +//! current link level rather than the hardware's latched-low status. +//! +//! Ported from hw/net/dp83840.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +use crate::snapshot::{field, field_bool, field_u32, field_u64, field_u8, toml_u16, StateTable}; +pub mod reg { + pub const BASIC_CONTROL: u8 = 0x0; + pub const BASIC_STATUS: u8 = 0x1; + pub const PHY_ID1: u8 = 0x2; + pub const PHY_ID2: u8 = 0x3; + pub const AUTO_NEG_ADVERTISE: u8 = 0x4; + pub const AUTO_NEG_PARTNER: u8 = 0x5; + pub const AUTO_NEG_EXPANSION: u8 = 0x6; + pub const NEXT_PAGE_TRANSMIT: u8 = 0x7; + pub const PHY_STATUS: u8 = 0x10; + pub const INTERRUPT_CONTROL: u8 = 0x11; + pub const INTERRUPT_STATUS: u8 = 0x12; + pub const DISCONNECT_COUNTER: u8 = 0x13; + pub const FALSE_CARRIER_COUNTER: u8 = 0x14; + pub const RECEIVE_ERROR_COUNTER: u8 = 0x15; + pub const PCS_CONFIG: u8 = 0x16; + pub const LOOPBACK: u8 = 0x17; + pub const TEN_BASE_T_STATUS: u8 = 0x18; + pub const PHY_CONTROL: u8 = 0x19; +} +const SETTLE: Duration = Duration::from_secs(2); +const BREAK: Duration = Duration::from_millis(1500); + +/// Management state for one transceiver. No board GPIO reset is connected. +pub struct Dp83843 { + configured_addr: u8, + regs: [u16; 32], + backend_up: bool, + done: bool, + forced: bool, + resolved: u8, + armed: bool, + deadline: Option, + complete: Option, + pending: bool, + restarting: bool, + pub unimplemented_accesses: u32, +} +impl Dp83843 { + pub fn new(addr: u8) -> Self { + let mut p = Self { + configured_addr: addr & 31, + regs: [0; 32], + backend_up: true, + done: false, + forced: false, + resolved: 0, + armed: false, + deadline: None, + complete: None, + pending: false, + restarting: false, + unimplemented_accesses: 0, + }; + p.reset(); + p + } + pub fn reset(&mut self) { + self.regs = [0; 32]; + self.regs[reg::BASIC_CONTROL as usize] = 0x3100 | if self.configured_addr == 0 { 0x400 } else { 0 }; + self.regs[reg::AUTO_NEG_ADVERTISE as usize] = 0x1e1; + self.regs[reg::NEXT_PAGE_TRANSMIT as usize] = 0x2001; + self.regs[reg::PCS_CONFIG as usize] = 0x20; + self.regs[reg::PHY_CONTROL as usize] = u16::from(self.configured_addr); + self.done = false; + self.forced = false; + self.resolved = 0; + self.armed = false; + self.deadline = None; + self.complete = None; + self.pending = false; + self.restarting = false; + } + pub fn address(&self) -> u8 { + (self.regs[reg::PHY_CONTROL as usize] & 31) as u8 + } + pub fn backend_up(&self) -> bool { + self.backend_up + } + pub fn deadline(&self) -> Option { + self.deadline + } + fn autoneg(&self) -> bool { + self.regs[reg::BASIC_CONTROL as usize] & 0x1000 != 0 + } + fn supported(&self) -> bool { + self.regs[reg::PHY_CONTROL as usize] & 0x220 == 0 + && self.regs[reg::TEN_BASE_T_STATUS as usize] & 0x1a20 == 0 + && self.regs[reg::LOOPBACK as usize] & 0x3f80 == 0 + } + fn link(&self) -> bool { + self.backend_up + && self.supported() + && self.regs[reg::BASIC_CONTROL as usize] & 0x800 == 0 + && if self.autoneg() { + self.done && self.resolved != 0 + } else { + self.forced + } + } + pub fn data_path(&self) -> bool { + self.supported() && self.regs[reg::BASIC_CONTROL as usize] & 0xc00 == 0 + } + pub fn wire_enabled(&self) -> bool { + self.data_path() && self.link() + } + fn loopback_selected(&self) -> bool { + self.regs[reg::BASIC_CONTROL as usize] & 0x4000 != 0 || self.regs[reg::LOOPBACK as usize] & 0x30 != 0 + } + pub fn in_loopback(&self) -> bool { + self.data_path() && self.loopback_selected() + } + fn status(&self) -> u16 { + let mut v = if self.autoneg() { 0x200 } else { 0 }; + if self.loopback_selected() { + v |= 8; + } + if !self.link() { + return v; + } + v |= 1; + let (speed, full) = if self.autoneg() { + v |= 0x20; + (self.resolved <= 2, self.resolved == 1 || self.resolved == 3) + } else { + (self.regs[reg::BASIC_CONTROL as usize] & 0x2000 != 0, self.regs[reg::BASIC_CONTROL as usize] & 0x100 != 0) + }; + if !speed { + v |= 2; + } + if full { + v |= 4; + } + if self.regs[reg::BASIC_CONTROL as usize] & 0x400 == 0 { + v |= 0x800; + } + v + } + fn event(&mut self, before: u16) { + let after = self.status(); + if self.regs[reg::INTERRUPT_CONTROL as usize] & 2 != 0 && ((before ^ after) & 1 != 0 || !before & after & 0x20 != 0) { + self.pending = true; + } + } + fn resolve(&mut self) { + self.resolved = 0; + if self.autoneg() && self.supported() { + for (i, bit) in [0x100, 0x80, 0x40, 0x20].iter().enumerate() { + if self.regs[reg::AUTO_NEG_ADVERTISE as usize] & bit != 0 { + self.resolved = i as u8 + 1; + break; + } + } + } + if self.resolved == 0 { + crate::dlog!(crate::devlog::LogModule::Ioc, "dp83843: no common supported advertised mode"); + } + } + pub fn sync(&mut self, now: Instant) { + if self.deadline.is_some_and(|d| now >= d) { + let before = self.status(); + if let Some(end) = self.complete.take() { + self.regs[reg::BASIC_CONTROL as usize] &= !0x200; + self.deadline = Some(end); + if now < end { + return; + } + } + self.deadline = None; + self.restarting = false; + self.done = true; + self.resolve(); + self.event(before); + } + } + pub fn set_backend_link(&mut self, up: bool, now: Instant) { + self.sync(now); + let before = self.status(); + self.backend_up = up; + self.event(before); + } + fn unimplemented(&mut self, r: u8) { + self.unimplemented_accesses += 1; + crate::dlog!(crate::devlog::LogModule::Ioc, "dp83843: unsupported register or control at {r:#x}"); + } + pub fn read(&mut self, r: u8, now: Instant) -> u16 { + self.sync(now); + let v = match r { + reg::BASIC_CONTROL => self.regs[reg::BASIC_CONTROL as usize], + reg::BASIC_STATUS => { + if !self.armed && !self.done && self.autoneg() { + self.armed = true; + self.deadline = Some(now + SETTLE); + } + let status = self.status(); + 0x7849 | if status & 1 != 0 { 4 } else { 0 } | (status & 0x20) + } + reg::PHY_ID1 => 0x2000, + reg::PHY_ID2 => 0x5c10, + reg::AUTO_NEG_ADVERTISE | reg::NEXT_PAGE_TRANSMIT => self.regs[r as usize], + reg::AUTO_NEG_PARTNER => { + if self.done { + 0x1e1 + } else { + 0 + } + } + reg::AUTO_NEG_EXPANSION => 4 | u16::from(self.done), + reg::PHY_STATUS => { + let v = self.status() | if self.pending { 0x100 } else { 0 }; + self.pending = false; + v + } + reg::INTERRUPT_CONTROL => self.regs[reg::INTERRUPT_CONTROL as usize], + reg::INTERRUPT_STATUS => { + let v = if self.pending { 0x8000 } else { 0 }; + self.pending = false; + v + } + reg::DISCONNECT_COUNTER..=reg::RECEIVE_ERROR_COUNTER => { + let v = self.regs[r as usize]; + self.regs[r as usize] = 0; + v + } + reg::PCS_CONFIG => { + let v = self.regs[reg::PCS_CONFIG as usize]; + if self.autoneg() { + v & !0x4000 + } else if self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + v | 0x4000 + } else { + v + } + } + reg::LOOPBACK => { + self.regs[reg::LOOPBACK as usize] + | if self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + 0x1000 + } else { + 0 + } + } + reg::TEN_BASE_T_STATUS => { + self.regs[reg::TEN_BASE_T_STATUS as usize] + | if self.status() & 4 != 0 || self.regs[reg::PHY_CONTROL as usize] & 0x200 != 0 { + 0x80 + } else { + 0 + } + } + reg::PHY_CONTROL => self.regs[reg::PHY_CONTROL as usize], + _ => { + self.unimplemented(r); + 0 + } + }; + if self.regs[reg::INTERRUPT_CONTROL as usize] & 1 != 0 { + self.pending = true; + } + v + } + pub fn write(&mut self, r: u8, val: u16, now: Instant) { + self.sync(now); + let before = self.status(); + match r { + reg::BASIC_CONTROL => { + let val = val & 0xff80; + if val & 0x8000 != 0 { + self.reset(); + return; + } + if val & 0x80 != 0 { + self.unimplemented(r); + } + self.regs[reg::LOOPBACK as usize] &= !0x380; + if val & 0x4000 != 0 { + self.regs[reg::LOOPBACK as usize] |= 0x30; + } + if val & 0x400 == 0 && val & 0x1200 == 0x1200 { + self.done = false; + self.resolved = 0; + self.forced = false; + self.armed = true; + self.restarting = true; + self.deadline = Some(now + BREAK); + self.complete = Some(now + BREAK + SETTLE); + self.regs[reg::BASIC_CONTROL as usize] = val; + } else if val & 0x400 == 0 && (self.restarting || self.forced && !self.autoneg()) { + if val & 0x1000 == 0 { + self.restarting = false; + self.complete = None; + self.deadline = None; + self.forced = val & 0x800 == 0; + } + self.regs[reg::BASIC_CONTROL as usize] = val & !0x200; + } else { + self.restarting = false; + self.complete = None; + self.deadline = None; + self.forced = false; + self.done = val & 0x400 == 0; + self.armed = self.done; + self.regs[reg::BASIC_CONTROL as usize] = val & !0x8200; + } + if !self.autoneg() { + self.resolved = 0; + self.done = false; + self.forced = self.supported() && self.regs[reg::BASIC_CONTROL as usize] & 0xc00 == 0; + } else if self.done && self.resolved == 0 { + self.resolve(); + } + } + reg::BASIC_STATUS..=reg::PHY_ID2 | reg::AUTO_NEG_PARTNER | reg::AUTO_NEG_EXPANSION | reg::PHY_STATUS | reg::INTERRUPT_STATUS => {} + reg::AUTO_NEG_ADVERTISE => self.regs[reg::AUTO_NEG_ADVERTISE as usize] = val & 0xa5ff, + reg::NEXT_PAGE_TRANSMIT => self.regs[reg::NEXT_PAGE_TRANSMIT as usize] = (val & 0xb7ff) | (self.regs[reg::NEXT_PAGE_TRANSMIT as usize] & 0x800), + reg::INTERRUPT_CONTROL => self.regs[reg::INTERRUPT_CONTROL as usize] = val & 3, + reg::DISCONNECT_COUNTER..=reg::RECEIVE_ERROR_COUNTER => self.regs[r as usize] = val, + reg::PCS_CONFIG => { + if val & !0x20 != 0 { + self.unimplemented(r); + } + let mask = if self.autoneg() || self.regs[reg::PHY_CONTROL as usize] & 0x20 != 0 { + 0xbfef + } else { + 0xffef + }; + self.regs[reg::PCS_CONFIG as usize] = (self.regs[reg::PCS_CONFIG as usize] & !mask) | (val & mask); + } + reg::LOOPBACK => { + if val & 0x4000 != 0 { + self.unimplemented(r); + } + self.regs[reg::LOOPBACK as usize] = val & 0x7fb0; + } + reg::TEN_BASE_T_STATUS => { + if val & 0x55c != 0 { + self.unimplemented(r); + } + self.regs[reg::TEN_BASE_T_STATUS as usize] = val & 0x1ffc; + } + reg::PHY_CONTROL => { + if val & !31 != 0 { + self.unimplemented(r); + } + self.regs[reg::PHY_CONTROL as usize] = val & 0x3bff; + } + _ => self.unimplemented(r), + } + if !self.supported() { + self.resolved = 0; + self.forced = false; + self.unimplemented(r); + } + self.event(before); + if self.regs[reg::INTERRUPT_CONTROL as usize] & 1 != 0 { + self.pending = true; + } + } +} +// The registers and negotiation progress for a snapshot. The two instants +// are saved as the time they had left. +impl Dp83843 { + fn instant_left(at: Option, now: Instant) -> u64 { + // Nanoseconds left plus one; zero for none. + at.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1) + } + + fn instant_from(left: u64, now: Instant) -> Option { + (left != 0).then(|| now + Duration::from_nanos(left - 1)) + } + + pub(crate) fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u8("configured_addr", self.configured_addr) + .u16s("regs", &self.regs) + .bool("backend_up", self.backend_up) + .bool("done", self.done) + .bool("forced", self.forced) + .u8("resolved", self.resolved) + .bool("armed", self.armed) + .u64("deadline", Self::instant_left(self.deadline, now)) + .u64("complete", Self::instant_left(self.complete, now)) + .bool("pending", self.pending) + .bool("restarting", self.restarting) + .u32("unimplemented_accesses", self.unimplemented_accesses) + .build() + } + + pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { + let items = field(v, "regs")?.as_array().ok_or("regs: not a list")?; + if items.len() != 32 { + return Err(format!("regs: {} entries, expected 32", items.len())); + } + let mut regs = [0u16; 32]; + for (r, item) in regs.iter_mut().zip(items) { + *r = toml_u16(item).ok_or("regs: an entry is out of range")?; + } + *self = Self { + configured_addr: field_u8(v, "configured_addr")?, + regs, + backend_up: field_bool(v, "backend_up")?, + done: field_bool(v, "done")?, + forced: field_bool(v, "forced")?, + resolved: field_u8(v, "resolved")?, + armed: field_bool(v, "armed")?, + deadline: Self::instant_from(field_u64(v, "deadline")?, now), + complete: Self::instant_from(field_u64(v, "complete")?, now), + pending: field_bool(v, "pending")?, + restarting: field_bool(v, "restarting")?, + unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, + }; + Ok(()) + } +} + +impl crate::dev::ioc3_eth::MiiPhy for Dp83843 { + fn address(&self) -> u8 { Dp83843::address(self) } + fn deadline(&self) -> Option { Dp83843::deadline(self) } + fn wire_enabled(&self) -> bool { Dp83843::wire_enabled(self) } + fn in_loopback(&self) -> bool { Dp83843::in_loopback(self) } + fn sync(&mut self, now: Instant) { Dp83843::sync(self, now) } + fn read(&mut self, register: u8, now: Instant) -> u16 { Dp83843::read(self, register, now) } + fn write(&mut self, register: u8, value: u16, now: Instant) { Dp83843::write(self, register, value, now) } + fn set_backend_link(&mut self, up: bool, now: Instant) { Dp83843::set_backend_link(self, up, now) } + fn power_on(&mut self) { Dp83843::reset(self) } + fn save_state(&self, now: Instant) -> toml::Value { Dp83843::save_state(self, now) } + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { Dp83843::load_state(self, v, now) } +} + +#[cfg(test)] +mod tests { + // Cases ported from tests/qtest/sgi-ioc3-eth-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + + /// The guest observes link and negotiation changes through MII status registers. + #[test] + fn the_phy_restarts_negotiation_and_resets_the_link() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + assert_eq!(p.read(reg::BASIC_STATUS, now) & 0x24, 0); + assert!(!p.wire_enabled()); + assert_eq!(p.read(reg::BASIC_STATUS, now + Duration::from_secs(2)) & 0x24, 0x24); + p.write(reg::BASIC_CONTROL, 0x3300, now + Duration::from_secs(3)); + for (millis, restart, link) in [(4499, true, false), (4500, false, false), (6500, false, true)] { + let at = now + Duration::from_millis(millis); + assert_eq!(p.read(reg::BASIC_CONTROL, at) & 0x200 != 0, restart); + assert_eq!(p.read(reg::BASIC_STATUS, at) & 4 != 0, link); + assert_eq!(p.wire_enabled(), link); + } + let at = now + Duration::from_secs(7); + p.set_backend_link(false, at); + p.write(reg::PHY_CONTROL, 7, at); + p.write(reg::BASIC_CONTROL, 0x8000, at); + assert_eq!(p.address(), 1); + assert_eq!(p.read(reg::BASIC_STATUS, at) & 4, 0); + assert_eq!(p.read(reg::BASIC_STATUS, at + Duration::from_secs(2)) & 4, 0); + p.set_backend_link(true, at + Duration::from_secs(2)); + assert!(p.wire_enabled()); + } + + /// A PROM probing the PHY reads the next-page register's reset value and + /// writes it back; the read-only toggle bit must survive every write. + #[test] + fn the_next_page_register_keeps_its_toggle_bit_and_resets_to_0x2001() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0x2001); + for bit in 0..16 { + let value = 1u16 << bit; + p.write(reg::NEXT_PAGE_TRANSMIT, value, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), value & 0xb7ff, "bit {bit}"); + } + p.write(reg::NEXT_PAGE_TRANSMIT, 0xffff, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0xb7ff); + p.write(reg::BASIC_CONTROL, 0x8000, now); + assert_eq!(p.read(reg::NEXT_PAGE_TRANSMIT, now), 0x2001); + } + + /// The PROM's PHY probe expects zero loopback and masked 10BASE-T status + /// registers, plus the standard BMSR and ANER capability bits, both at + /// power-on and after a BMCR reset. + #[test] + fn the_phy_reads_the_probe_values_the_prom_expects_before_and_after_reset() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + for round in 0..2 { + assert_eq!(p.read(reg::BASIC_STATUS, now) & 0x7849, 0x7849, "round {round}"); + assert_eq!(p.read(reg::AUTO_NEG_EXPANSION, now) & 4, 4); + assert_eq!(p.read(reg::LOOPBACK, now), 0); + assert_eq!(p.read(reg::TEN_BASE_T_STATUS, now) & 0xc77f, 0); + p.write(reg::BASIC_CONTROL, 0x2100, now); + p.write(reg::LOOPBACK, 0x20, now); + p.write(reg::BASIC_CONTROL, 0x8000, now); + } + assert_eq!(p.unimplemented_accesses, 0); + } + + /// Setting BMCR loopback makes the PHY report loopback in the LBR bits the + /// MAC consults, and leaving it must drop the stale bypass bits. + #[test] + fn bmcr_loopback_selects_the_loopback_register_and_clears_the_bypass_bits() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + p.write(reg::LOOPBACK, 0x0180, now); + p.write(reg::BASIC_CONTROL, 0x7300, now); + assert_eq!(p.read(reg::LOOPBACK, now) & 0x03b0, 0x0030); + assert!(p.in_loopback()); + let lbr = p.read(reg::LOOPBACK, now); + p.write(reg::LOOPBACK, lbr | 0x0180, now); + p.write(reg::BASIC_CONTROL, 0x3100, now); + assert_eq!(p.read(reg::LOOPBACK, now) & 0x0380, 0); + p.write(reg::BASIC_CONTROL, 0x8000, now); + assert_eq!(p.read(reg::LOOPBACK, now), 0); + assert!(!p.in_loopback()); + } + + /// Drivers poll the error counters and expect each read to return the count + /// since the previous read, not a running total. + #[test] + fn the_error_counters_clear_when_read() { + let now = Instant::now(); + let mut p = Dp83843::new(1); + for r in [reg::DISCONNECT_COUNTER, reg::FALSE_CARRIER_COUNTER, reg::RECEIVE_ERROR_COUNTER] { + p.write(r, 0xbeef, now); + assert_eq!(p.read(r, now), 0xbeef); + assert_eq!(p.read(r, now), 0); + } + } + + /// A guest that restarts negotiation twice in a row must see the break + /// interval from the second write, not a shortened one from the first. + #[test] + fn a_second_restart_request_rearms_the_break_interval() { + let now = Instant::now(); + let ms = |m: u64| now + Duration::from_millis(m); + let mut p = Dp83843::new(1); + p.write(reg::BASIC_CONTROL, 0x3200, ms(0)); + p.write(reg::BASIC_CONTROL, 0x3200, ms(750)); + assert_ne!(p.read(reg::BASIC_CONTROL, ms(2249)) & 0x200, 0); + assert_eq!(p.read(reg::BASIC_CONTROL, ms(2250)) & 0x200, 0); + assert_eq!(p.read(reg::BASIC_STATUS, ms(4249)) & 0x20, 0); + assert_eq!(p.read(reg::BASIC_STATUS, ms(4250)) & 0x20, 0x20); + } + + /// A snapshot keeps the registers, the negotiation in progress and its + /// time left. + #[test] + fn a_snapshot_round_trips_a_negotiation_in_progress() { + let now = Instant::now(); + let mut src = Dp83843::new(1); + src.write(reg::BASIC_CONTROL, 0x3300, now); + let v = src.save_state(now); + let mut dst = Dp83843::new(1); + dst.load_state(&v, now).unwrap(); + assert_eq!(v, dst.save_state(now)); + assert_eq!(src.deadline(), dst.deadline()); + assert!(dst.load_state(&toml::Value::Table(Default::default()), now).is_err()); + } +} diff --git a/src/dev/ics1890.rs b/src/dev/ics1890.rs new file mode 100644 index 000000000..04cc6706a --- /dev/null +++ b/src/dev/ics1890.rs @@ -0,0 +1,423 @@ +//! The ICS1890 transceiver fitted to Octane's IOC3. +//! +//! This module is shared by the Octane IOC3 Ethernet model. It was moved here +//! from `ioc3_eth.rs`; its management-register behavior follows the existing +//! Octane behavior and Integrated Circuit Systems, "ICS1890 10Base-T/100Base-TX +//! Integrated PHYceiver", document ICS1890RevG, 21 October 1997. +//! +//! Ported from hw/net/ics1890.c in jamesbraid/qemu-sgi-origami. + +use std::time::{Duration, Instant}; + +use crate::dev::ioc3_eth::MiiPhy; +use crate::snapshot::{field_bool, field_u16, field_u32, field_u64, field_u8, StateTable}; + +/// Ordinary and command-override writable masks, by register. +fn phy_masks(r: usize) -> (u16, u16) { + match r { + 0 => (0xff80, 0), + 1 => (0, 0x7f80), + 2 | 3 => (0, 0xffff), + 4 => (0x21e0, 0x1c1f), + 6 => (0, 0xffe0), + 0x10 => (0x802d, 0), + 0x12 => (0x002f, 0), + 0x13 => (0x0011, 0), + _ => (0, 0), + } +} + +/// The ICS1890 as the Octane carries it: address 1, node mode, software +/// priority, revision 3. +pub struct Ics1890 { + addr: u8, + bmcr: u16, + bmsr_caps: u16, + id1: u16, + id2: u16, + anar: u16, + partner: u16, + exp: u16, + ext_ctl: u16, + tenbt: u16, + ext2: u16, + override_armed: bool, + page_received: bool, + link_low_latch: bool, + last_link: bool, + backend_up: bool, + nego_done: bool, + deadline: Option, + delay: Duration, + /// Accesses to registers the part does not implement. + pub unimplemented_accesses: u32, +} + +impl Ics1890 { + pub fn new(addr: u8, delay: Duration) -> Self { + let mut p = Self { + addr, + bmcr: 0, + bmsr_caps: 0, + id1: 0, + id2: 0, + anar: 0, + partner: 0, + exp: 0, + ext_ctl: 0, + tenbt: 0, + ext2: 0, + override_armed: false, + page_received: false, + link_low_latch: false, + last_link: false, + backend_up: true, + nego_done: false, + deadline: None, + delay, + unimplemented_accesses: 0, + }; + p.reset(Instant::now()); + p + } + + pub fn address(&self) -> u8 { + self.addr + } + + /// Hardware and software reset: defaults, no override, no latches, and a + /// fresh negotiation if enabled. + pub fn reset(&mut self, now: Instant) { + self.bmcr = 0x3000; + self.bmsr_caps = 0x7800; + self.id1 = 0x0015; + self.id2 = 0xf423; + self.anar = 0x01e1; + self.partner = 0; + self.exp = 0; + self.ext_ctl = 0x0008; + self.tenbt = 0; + self.ext2 = 0x0001; + self.override_armed = false; + self.page_received = false; + self.link_low_latch = false; + self.nego_done = false; + self.deadline = None; + self.last_link = false; + self.start_negotiation(now); + } + + fn autoneg(&self) -> bool { + self.bmcr & 0x1000 != 0 + } + fn power_down(&self) -> bool { + self.bmcr & 0x0800 != 0 + } + fn isolated(&self) -> bool { + self.bmcr & 0x0400 != 0 + } + + /// The live link: down in power-down, down without a backend, and with + /// negotiation enabled up only once it has completed. + fn link(&self) -> bool { + !self.power_down() && self.backend_up && (!self.autoneg() || self.nego_done) + } + + fn start_negotiation(&mut self, now: Instant) { + self.nego_done = false; + self.partner = 0; + self.page_received = false; + self.deadline = None; + if self.autoneg() && self.backend_up && !self.power_down() { + // The synthetic peer advertises 0x01e1: IEEE 802.3, no fault. + let common = self.anar & 0x01e0; + if self.anar & 0x1f == 1 && common != 0 && self.anar & 0x2000 == 0 { + self.deadline = Some(now + self.delay); + } + } + } + + /// When a negotiation in progress completes, if one is. + pub fn deadline(&self) -> Option { + self.deadline + } + + /// Advance the negotiation deadline and note link transitions. Reads never + /// start or postpone negotiation; this only observes the clock. + pub fn sync(&mut self, now: Instant) { + if let Some(d) = self.deadline { + if now >= d && !self.power_down() && self.backend_up { + self.deadline = None; + self.nego_done = true; + self.partner = 0x01e1; + self.page_received = true; + } + } + let link = self.link(); + if self.last_link && !link { + self.link_low_latch = true; + } + self.last_link = link; + } + + /// The packet backend's cable state. + pub fn set_backend_link(&mut self, up: bool, now: Instant) { + self.sync(now); + if self.backend_up == up { + return; + } + self.backend_up = up; + if up { + // Recovery starts a fresh negotiation. + self.start_negotiation(now); + } else { + self.deadline = None; + self.nego_done = false; + } + self.sync(now); + } + + /// BMCR loopback turns frames round inside the part. Isolate and + /// power-down take precedence. + pub fn in_loopback(&self) -> bool { + self.bmcr & 0x4000 != 0 && !self.power_down() && !self.isolated() + } + + /// Frames may cross the MII at all. + pub fn data_path(&self) -> bool { + !self.power_down() && !self.isolated() + } + + /// Frames may go to or come from the packet backend. + pub fn wire_enabled(&self) -> bool { + self.data_path() && self.link() + } + + fn resolved(&self) -> (bool, bool) { + // (100 Mb/s, full duplex) + if self.autoneg() { + let common = self.anar & self.partner & 0x01e0; + if common & 0x100 != 0 { + (true, true) + } else if common & 0x80 != 0 { + (true, false) + } else if common & 0x40 != 0 { + (false, true) + } else { + (false, false) + } + } else { + (self.bmcr & 0x2000 != 0, self.bmcr & 0x0100 != 0) + } + } + + pub fn read(&mut self, r: u8, now: Instant) -> u16 { + self.sync(now); + match r { + 0 => self.bmcr, + 1 => { + let mut v = 0x0009 | self.bmsr_caps; + if self.autoneg() && self.nego_done { + v |= 0x0020; + } + if self.link() && !self.link_low_latch { + v |= 0x0004; + } + self.link_low_latch = false; + v + } + 2 => self.id1, + 3 => self.id2, + 4 => self.anar, + 5 => self.partner, + 6 => { + let v = (self.exp & 0xffe0) + | if self.nego_done { 1 } else { 0 } + | if self.page_received { 2 } else { 0 }; + self.page_received = false; + v + } + 0x10 => { + // Bit 4 is a fixed reserved read choice (stock firmware + // expects it set); bit 15 reads the override state. + (self.ext_ctl & 0x002d) + | (u16::from(self.addr & 0x1f) << 6) + | 0x0010 + | if self.override_armed { 0x8000 } else { 0 } + } + 0x11 => { + let link = self.link(); + let (speed, full) = self.resolved(); + let mut v = 0; + if link { + v |= 1; + if speed { + v |= 0x8000; + } + if full { + v |= 0x4000; + } + } + if self.nego_done { + v |= 0x0010 | (7 << 11); + } + v + } + 0x12 => (self.tenbt & 0x002f) | 0x0010, + // Node mode strap in bit 14, software bit 0. + 0x13 => (self.ext2 & 0x0011) | 0x4000, + _ => { + self.unimplemented_accesses += 1; + 0 + } + } + } + + pub fn write(&mut self, r: u8, val: u16, now: Instant) { + self.sync(now); + let r = usize::from(r); + let armed = self.override_armed; + match r { + 0 => { + if val & 0x8000 != 0 { + self.reset(now); + self.sync(now); + return; + } + let old = self.bmcr; + // Reset and restart are self-clearing. + self.bmcr = val & 0x7d80; + let restart = val & 0x0200 != 0; + let pd_now = self.power_down(); + if pd_now && old & 0x0800 == 0 { + // Entering power-down clears the latching status. + self.link_low_latch = false; + self.page_received = false; + self.deadline = None; + } else if !pd_now && old & 0x0800 != 0 { + // Release: enabled negotiation starts afresh. + self.start_negotiation(now); + } else if self.autoneg() && (restart || old & 0x1000 == 0) { + self.start_negotiation(now); + } else if !self.autoneg() { + self.nego_done = false; + self.deadline = None; + } + if armed { + self.override_armed = false; + } + } + 1 | 2 | 3 | 4 | 6 | 0x10 | 0x12 | 0x13 => { + let (ord, ovr) = phy_masks(r); + let mask = ord | if armed { ovr } else { 0 }; + let cell = match r { + 1 => &mut self.bmsr_caps, + 2 => &mut self.id1, + 3 => &mut self.id2, + 4 => &mut self.anar, + 6 => &mut self.exp, + 0x10 => &mut self.ext_ctl, + 0x12 => &mut self.tenbt, + _ => &mut self.ext2, + }; + let before = *cell; + *cell = (before & !mask) | (val & mask); + // The status caps live in bits 14..7 only. + if r == 1 { + self.bmsr_caps &= 0x7f80; + } + if r == 4 && self.anar != before && self.autoneg() { + self.start_negotiation(now); + } + if armed { + self.override_armed = false; + } else if r == 0x10 && val & 0x8000 != 0 { + self.override_armed = true; + } + } + _ => { + // Partner ability, Quick Poll and unimplemented registers + // ignore writes. The part is addressed, so a write still + // consumes an armed override. + if r != 5 && r != 0x11 { + self.unimplemented_accesses += 1; + } + if armed { + self.override_armed = false; + } + } + } + self.sync(now); + } +} + +// The transceiver's registers and auto-negotiation progress for a +// snapshot. Its one deadline is saved as the time it had left. +impl Ics1890 { + pub(crate) fn save_state(&self, now: Instant) -> toml::Value { + StateTable::new() + .u8("addr", self.addr) + .u16("bmcr", self.bmcr) + .u16("bmsr_caps", self.bmsr_caps) + .u16("id1", self.id1) + .u16("id2", self.id2) + .u16("anar", self.anar) + .u16("partner", self.partner) + .u16("exp", self.exp) + .u16("ext_ctl", self.ext_ctl) + .u16("tenbt", self.tenbt) + .u16("ext2", self.ext2) + .bool("override_armed", self.override_armed) + .bool("page_received", self.page_received) + .bool("link_low_latch", self.link_low_latch) + .bool("last_link", self.last_link) + .bool("backend_up", self.backend_up) + .bool("nego_done", self.nego_done) + // Nanoseconds left plus one; zero for none. + .u64("deadline", self.deadline.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1)) + .u64("delay", self.delay.as_nanos() as u64) + .u32("unimplemented_accesses", self.unimplemented_accesses) + .build() + } + + pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { + let deadline = field_u64(v, "deadline")?; + *self = Self { + addr: field_u8(v, "addr")?, + bmcr: field_u16(v, "bmcr")?, + bmsr_caps: field_u16(v, "bmsr_caps")?, + id1: field_u16(v, "id1")?, + id2: field_u16(v, "id2")?, + anar: field_u16(v, "anar")?, + partner: field_u16(v, "partner")?, + exp: field_u16(v, "exp")?, + ext_ctl: field_u16(v, "ext_ctl")?, + tenbt: field_u16(v, "tenbt")?, + ext2: field_u16(v, "ext2")?, + override_armed: field_bool(v, "override_armed")?, + page_received: field_bool(v, "page_received")?, + link_low_latch: field_bool(v, "link_low_latch")?, + last_link: field_bool(v, "last_link")?, + backend_up: field_bool(v, "backend_up")?, + nego_done: field_bool(v, "nego_done")?, + deadline: (deadline != 0).then(|| now + Duration::from_nanos(deadline - 1)), + delay: Duration::from_nanos(field_u64(v, "delay")?), + unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, + }; + Ok(()) + } +} + +impl MiiPhy for Ics1890 { + fn address(&self) -> u8 { Ics1890::address(self) } + fn deadline(&self) -> Option { Ics1890::deadline(self) } + fn wire_enabled(&self) -> bool { Ics1890::wire_enabled(self) } + fn in_loopback(&self) -> bool { Ics1890::in_loopback(self) } + fn sync(&mut self, now: Instant) { Ics1890::sync(self, now) } + fn read(&mut self, register: u8, now: Instant) -> u16 { Ics1890::read(self, register, now) } + fn write(&mut self, register: u8, value: u16, now: Instant) { Ics1890::write(self, register, value, now) } + fn set_backend_link(&mut self, up: bool, now: Instant) { Ics1890::set_backend_link(self, up, now) } + fn power_on(&mut self) { Ics1890::reset(self, Instant::now()) } + fn save_state(&self, now: Instant) -> toml::Value { Ics1890::save_state(self, now) } + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { Ics1890::load_state(self, v, now) } +} diff --git a/src/dev/ioc3_eth.rs b/src/dev/ioc3_eth.rs index cbe3c0c88..4b9e9fa63 100644 --- a/src/dev/ioc3_eth.rs +++ b/src/dev/ioc3_eth.rs @@ -1,5 +1,5 @@ -//! The Ethernet MAC inside IOC3, with the ICS1890 transceiver the Octane -//! carries on its management bus. +//! The Ethernet MAC inside IOC3, with the Octane ICS1890 or Fuel DP83843 +//! transceiver on its management bus. //! //! IOC3 forwards the register offsets 0xf0..=0x150 of its register BAR here //! through [`Ioc3EthPort`]. The MAC is SGI's own design with no vendor @@ -17,7 +17,7 @@ //! Ring entries and descriptor words are big-endian bytes at bus addresses, //! frame bytes are in wire order; the Bridge applies any programmed swap. //! -//! Frames go through iris's network engine like the O2's MEC does: see +//! Frames use iris's shared network link. The machine attaches it through //! [`Ioc3Eth::attach`]. //! //! Approximations (the QEMU model this was ported from has the same ones, @@ -38,8 +38,8 @@ //! sends whenever the PHY's data path is open (not powered down or //! isolated), link or not. //! -//! Ported from hw/net/sgi-ioc3-eth.c and hw/net/ics1890.c in -//! jamesbraid/qemu-sgi-origami. +//! Ported from hw/net/sgi-ioc3-eth.c in jamesbraid/qemu-sgi-origami. The +//! Octane PHY model lives in `ics1890.rs`. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, OnceLock, Weak}; @@ -48,8 +48,9 @@ use parking_lot::Mutex; use std::time::{Duration, Instant}; use crate::dev::ioc3_port::Ioc3EthPort; +use crate::dev::ics1890::Ics1890; use crate::pci::{PciDma, PciDmaStatus}; -use crate::snapshot::{field, field_bool, field_u16, field_u32, field_u32s, field_u64, field_u8, StateTable}; +use crate::snapshot::{field, field_bool, field_u16, field_u32, field_u32s, field_u64, StateTable}; use crate::traits::{Device, Resettable, Saveable}; #[cfg(test)] @@ -163,6 +164,23 @@ pub const PHY_ADDR: u8 = 1; /// How long the synthetic link partner takes to complete negotiation. pub const NEGOTIATION_DELAY: Duration = Duration::from_millis(100); +/// The management and packet-path behavior that varies between IOC3 PHYs. +pub(crate) trait MiiPhy: Send { + fn address(&self) -> u8; + fn deadline(&self) -> Option; + fn wire_enabled(&self) -> bool; + fn in_loopback(&self) -> bool; + fn sync(&mut self, now: Instant); + fn read(&mut self, register: u8, now: Instant) -> u16; + fn write(&mut self, register: u8, value: u16, now: Instant); + fn set_backend_link(&mut self, up: bool, now: Instant); + fn power_on(&mut self); + /// The transceiver's registers and negotiation progress for a snapshot; + /// its deadline is saved as the time it had left. + fn save_state(&self, now: Instant) -> toml::Value; + fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String>; +} + fn ridx(off: u32) -> usize { ((off - MAC_CONTROL) / 4) as usize } @@ -171,345 +189,6 @@ fn ridx(off: u32) -> usize { // ICS1890 transceiver // --------------------------------------------------------------------------- -/// Ordinary and command-override writable masks, by register. -fn phy_masks(r: usize) -> (u16, u16) { - match r { - 0 => (0xff80, 0), - 1 => (0, 0x7f80), - 2 | 3 => (0, 0xffff), - 4 => (0x21e0, 0x1c1f), - 6 => (0, 0xffe0), - 0x10 => (0x802d, 0), - 0x12 => (0x002f, 0), - 0x13 => (0x0011, 0), - _ => (0, 0), - } -} - -/// The ICS1890 as the Octane carries it: address 1, node mode, software -/// priority, revision 3. -pub struct Ics1890 { - addr: u8, - bmcr: u16, - bmsr_caps: u16, - id1: u16, - id2: u16, - anar: u16, - partner: u16, - exp: u16, - ext_ctl: u16, - tenbt: u16, - ext2: u16, - override_armed: bool, - page_received: bool, - link_low_latch: bool, - last_link: bool, - backend_up: bool, - nego_done: bool, - deadline: Option, - delay: Duration, - /// Accesses to registers the part does not implement. - pub unimplemented_accesses: u32, -} - -impl Ics1890 { - pub fn new(addr: u8, delay: Duration) -> Self { - let mut p = Self { - addr, - bmcr: 0, - bmsr_caps: 0, - id1: 0, - id2: 0, - anar: 0, - partner: 0, - exp: 0, - ext_ctl: 0, - tenbt: 0, - ext2: 0, - override_armed: false, - page_received: false, - link_low_latch: false, - last_link: false, - backend_up: true, - nego_done: false, - deadline: None, - delay, - unimplemented_accesses: 0, - }; - p.reset(Instant::now()); - p - } - - pub fn address(&self) -> u8 { - self.addr - } - - /// Hardware and software reset: defaults, no override, no latches, and a - /// fresh negotiation if enabled. - pub fn reset(&mut self, now: Instant) { - self.bmcr = 0x3000; - self.bmsr_caps = 0x7800; - self.id1 = 0x0015; - self.id2 = 0xf423; - self.anar = 0x01e1; - self.partner = 0; - self.exp = 0; - self.ext_ctl = 0x0008; - self.tenbt = 0; - self.ext2 = 0x0001; - self.override_armed = false; - self.page_received = false; - self.link_low_latch = false; - self.nego_done = false; - self.deadline = None; - self.last_link = false; - self.start_negotiation(now); - } - - fn autoneg(&self) -> bool { - self.bmcr & 0x1000 != 0 - } - fn power_down(&self) -> bool { - self.bmcr & 0x0800 != 0 - } - fn isolated(&self) -> bool { - self.bmcr & 0x0400 != 0 - } - - /// The live link: down in power-down, down without a backend, and with - /// negotiation enabled up only once it has completed. - fn link(&self) -> bool { - !self.power_down() && self.backend_up && (!self.autoneg() || self.nego_done) - } - - fn start_negotiation(&mut self, now: Instant) { - self.nego_done = false; - self.partner = 0; - self.page_received = false; - self.deadline = None; - if self.autoneg() && self.backend_up && !self.power_down() { - // The synthetic peer advertises 0x01e1: IEEE 802.3, no fault. - let common = self.anar & 0x01e0; - if self.anar & 0x1f == 1 && common != 0 && self.anar & 0x2000 == 0 { - self.deadline = Some(now + self.delay); - } - } - } - - /// When a negotiation in progress completes, if one is. - pub fn deadline(&self) -> Option { - self.deadline - } - - /// Advance the negotiation deadline and note link transitions. Reads never - /// start or postpone negotiation; this only observes the clock. - pub fn sync(&mut self, now: Instant) { - if let Some(d) = self.deadline { - if now >= d && !self.power_down() && self.backend_up { - self.deadline = None; - self.nego_done = true; - self.partner = 0x01e1; - self.page_received = true; - } - } - let link = self.link(); - if self.last_link && !link { - self.link_low_latch = true; - } - self.last_link = link; - } - - /// The packet backend's cable state. - pub fn set_backend_link(&mut self, up: bool, now: Instant) { - self.sync(now); - if self.backend_up == up { - return; - } - self.backend_up = up; - if up { - // Recovery starts a fresh negotiation. - self.start_negotiation(now); - } else { - self.deadline = None; - self.nego_done = false; - } - self.sync(now); - } - - /// BMCR loopback turns frames round inside the part. Isolate and - /// power-down take precedence. - pub fn in_loopback(&self) -> bool { - self.bmcr & 0x4000 != 0 && !self.power_down() && !self.isolated() - } - - /// Frames may cross the MII at all. - pub fn data_path(&self) -> bool { - !self.power_down() && !self.isolated() - } - - /// Frames may go to or come from the packet backend. - pub fn wire_enabled(&self) -> bool { - self.data_path() && self.link() - } - - fn resolved(&self) -> (bool, bool) { - // (100 Mb/s, full duplex) - if self.autoneg() { - let common = self.anar & self.partner & 0x01e0; - if common & 0x100 != 0 { - (true, true) - } else if common & 0x80 != 0 { - (true, false) - } else if common & 0x40 != 0 { - (false, true) - } else { - (false, false) - } - } else { - (self.bmcr & 0x2000 != 0, self.bmcr & 0x0100 != 0) - } - } - - pub fn read(&mut self, r: u8, now: Instant) -> u16 { - self.sync(now); - match r { - 0 => self.bmcr, - 1 => { - let mut v = 0x0009 | self.bmsr_caps; - if self.autoneg() && self.nego_done { - v |= 0x0020; - } - if self.link() && !self.link_low_latch { - v |= 0x0004; - } - self.link_low_latch = false; - v - } - 2 => self.id1, - 3 => self.id2, - 4 => self.anar, - 5 => self.partner, - 6 => { - let v = (self.exp & 0xffe0) - | if self.nego_done { 1 } else { 0 } - | if self.page_received { 2 } else { 0 }; - self.page_received = false; - v - } - 0x10 => { - // Bit 4 is a fixed reserved read choice (stock firmware - // expects it set); bit 15 reads the override state. - (self.ext_ctl & 0x002d) - | (u16::from(self.addr & 0x1f) << 6) - | 0x0010 - | if self.override_armed { 0x8000 } else { 0 } - } - 0x11 => { - let link = self.link(); - let (speed, full) = self.resolved(); - let mut v = 0; - if link { - v |= 1; - if speed { - v |= 0x8000; - } - if full { - v |= 0x4000; - } - } - if self.nego_done { - v |= 0x0010 | (7 << 11); - } - v - } - 0x12 => (self.tenbt & 0x002f) | 0x0010, - // Node mode strap in bit 14, software bit 0. - 0x13 => (self.ext2 & 0x0011) | 0x4000, - _ => { - self.unimplemented_accesses += 1; - 0 - } - } - } - - pub fn write(&mut self, r: u8, val: u16, now: Instant) { - self.sync(now); - let r = usize::from(r); - let armed = self.override_armed; - match r { - 0 => { - if val & 0x8000 != 0 { - self.reset(now); - self.sync(now); - return; - } - let old = self.bmcr; - // Reset and restart are self-clearing. - self.bmcr = val & 0x7d80; - let restart = val & 0x0200 != 0; - let pd_now = self.power_down(); - if pd_now && old & 0x0800 == 0 { - // Entering power-down clears the latching status. - self.link_low_latch = false; - self.page_received = false; - self.deadline = None; - } else if !pd_now && old & 0x0800 != 0 { - // Release: enabled negotiation starts afresh. - self.start_negotiation(now); - } else if self.autoneg() && (restart || old & 0x1000 == 0) { - self.start_negotiation(now); - } else if !self.autoneg() { - self.nego_done = false; - self.deadline = None; - } - if armed { - self.override_armed = false; - } - } - 1 | 2 | 3 | 4 | 6 | 0x10 | 0x12 | 0x13 => { - let (ord, ovr) = phy_masks(r); - let mask = ord | if armed { ovr } else { 0 }; - let cell = match r { - 1 => &mut self.bmsr_caps, - 2 => &mut self.id1, - 3 => &mut self.id2, - 4 => &mut self.anar, - 6 => &mut self.exp, - 0x10 => &mut self.ext_ctl, - 0x12 => &mut self.tenbt, - _ => &mut self.ext2, - }; - let before = *cell; - *cell = (before & !mask) | (val & mask); - // The status caps live in bits 14..7 only. - if r == 1 { - self.bmsr_caps &= 0x7f80; - } - if r == 4 && self.anar != before && self.autoneg() { - self.start_negotiation(now); - } - if armed { - self.override_armed = false; - } else if r == 0x10 && val & 0x8000 != 0 { - self.override_armed = true; - } - } - _ => { - // Partner ability, Quick Poll and unimplemented registers - // ignore writes. The part is addressed, so a write still - // consumes an armed override. - if r != 5 && r != 0x11 { - self.unimplemented_accesses += 1; - } - if armed { - self.override_armed = false; - } - } - } - self.sync(now); - } -} - // --------------------------------------------------------------------------- // Checksums and the multicast hash // --------------------------------------------------------------------------- @@ -616,7 +295,7 @@ struct State { mii_data_w: u16, /// Apart from its index: an index-only write must not disarm. rx_timer_armed: bool, - phy: Ics1890, + phy: Box, irq_asserted: bool, /// Level changes not yet delivered to the callback, in order. irq_events: Vec, @@ -626,14 +305,14 @@ struct State { } impl State { - fn new(delay: Duration) -> Self { + fn new(phy: Box) -> Self { Self { regs: [0; NREGS], int_deferred: 0, mii_data: MII_ABSENT, mii_data_w: 0, rx_timer_armed: false, - phy: Ics1890::new(PHY_ADDR, delay), + phy, irq_asserted: false, irq_events: Vec::new(), frame: vec![0; FRAME_MAX], @@ -642,6 +321,20 @@ impl State { } } + fn power_on(&mut self) { + self.phy.power_on(); + self.regs = [0; NREGS]; + self.int_deferred = 0; + self.mii_data = MII_ABSENT; + self.mii_data_w = 0; + self.rx_timer_armed = false; + self.irq_asserted = false; + self.irq_events.clear(); + self.frame.fill(0); + self.tx_frames = 0; + self.rx_frames = 0; + } + fn get(&self, off: u32) -> u32 { self.regs[ridx(off)] } @@ -1025,6 +718,7 @@ type IrqFn = Box; /// through the Bridge and HEART, which never call back into this device. pub struct Ioc3Eth { mac: [u8; 6], + thread_prefix: &'static str, st: Mutex, dma: Mutex>>, irq: Mutex>, @@ -1062,9 +756,19 @@ impl Ioc3Eth { /// As [`Ioc3Eth::new`], with the PHY's synthetic negotiation delay set. pub fn with_negotiation_delay(mac: [u8; 6], delay: Duration) -> Arc { + Self::with_phy(mac, Box::new(Ics1890::new(PHY_ADDR, delay)), "ip30-eth") + } + + /// Fuel uses the DP83843 at management address 1. + pub fn new_fuel(mac: [u8; 6]) -> Arc { + Self::with_phy(mac, Box::new(crate::dev::dp83843::Dp83843::new(PHY_ADDR)), "ioc3-eth") + } + + fn with_phy(mac: [u8; 6], phy: Box, thread_prefix: &'static str) -> Arc { Arc::new_cyclic(|me| Self { mac, - st: Mutex::new(State::new(delay)), + thread_prefix, + st: Mutex::new(State::new(phy)), dma: Mutex::new(None), irq: Mutex::new(None), irq_order: Mutex::new(()), @@ -1101,13 +805,7 @@ impl Ioc3Eth { /// with the state saying low. pub fn reset(&self) { let _order = self.irq_order.lock(); - { - let mut st = self.st.lock(); - let delay = st.phy.delay; - let backend_up = st.phy.backend_up; - *st = State::new(delay); - st.phy.set_backend_link(backend_up, Instant::now()); - } + self.st.lock().power_on(); if let Some(cb) = self.irq.lock().as_ref() { cb(false); } @@ -1175,8 +873,9 @@ impl Ioc3Eth { fn spawn_nat(&self) { let slot = self.backend.clone(); let Some(mut engine) = slot.lock().take() else { return }; + let thread_name = format!("{}-net", self.thread_prefix); let t = std::thread::Builder::new() - .name("ip30-eth-net".into()) + .name(thread_name) .spawn(move || { engine.run(); *slot.lock() = Some(engine); @@ -1281,8 +980,9 @@ impl Device for Ioc3Eth { } let wake = self.rx_wake.lock().get_or_insert_with(crate::net::Wake::new).clone(); let (me, running) = (self.me.clone(), self.running.clone()); + let thread_name = format!("{}-rx", self.thread_prefix); let rx = std::thread::Builder::new() - .name("ip30-eth-rx".into()) + .name(thread_name) .spawn(move || Self::rx_loop(me, running, wake)) .expect("ioc3 eth rx thread"); self.threads.lock().push(rx); @@ -1377,63 +1077,6 @@ impl Ioc3EthPort for Ioc3Eth { } } -// The transceiver's registers and auto-negotiation progress for a -// snapshot. Its one deadline is saved as the time it had left. -impl Ics1890 { - pub(crate) fn save_state(&self, now: Instant) -> toml::Value { - StateTable::new() - .u8("addr", self.addr) - .u16("bmcr", self.bmcr) - .u16("bmsr_caps", self.bmsr_caps) - .u16("id1", self.id1) - .u16("id2", self.id2) - .u16("anar", self.anar) - .u16("partner", self.partner) - .u16("exp", self.exp) - .u16("ext_ctl", self.ext_ctl) - .u16("tenbt", self.tenbt) - .u16("ext2", self.ext2) - .bool("override_armed", self.override_armed) - .bool("page_received", self.page_received) - .bool("link_low_latch", self.link_low_latch) - .bool("last_link", self.last_link) - .bool("backend_up", self.backend_up) - .bool("nego_done", self.nego_done) - // Nanoseconds left plus one; zero for none. - .u64("deadline", self.deadline.map_or(0, |d| d.saturating_duration_since(now).as_nanos() as u64 + 1)) - .u64("delay", self.delay.as_nanos() as u64) - .u32("unimplemented_accesses", self.unimplemented_accesses) - .build() - } - - pub(crate) fn load_state(&mut self, v: &toml::Value, now: Instant) -> Result<(), String> { - let deadline = field_u64(v, "deadline")?; - *self = Self { - addr: field_u8(v, "addr")?, - bmcr: field_u16(v, "bmcr")?, - bmsr_caps: field_u16(v, "bmsr_caps")?, - id1: field_u16(v, "id1")?, - id2: field_u16(v, "id2")?, - anar: field_u16(v, "anar")?, - partner: field_u16(v, "partner")?, - exp: field_u16(v, "exp")?, - ext_ctl: field_u16(v, "ext_ctl")?, - tenbt: field_u16(v, "tenbt")?, - ext2: field_u16(v, "ext2")?, - override_armed: field_bool(v, "override_armed")?, - page_received: field_bool(v, "page_received")?, - link_low_latch: field_bool(v, "link_low_latch")?, - last_link: field_bool(v, "last_link")?, - backend_up: field_bool(v, "backend_up")?, - nego_done: field_bool(v, "nego_done")?, - deadline: (deadline != 0).then(|| now + Duration::from_nanos(deadline - 1)), - delay: Duration::from_nanos(field_u64(v, "delay")?), - unimplemented_accesses: field_u32(v, "unimplemented_accesses")?, - }; - Ok(()) - } -} - // The Ethernet block's state for a snapshot: the MAC's registers, the // MII port and the transceiver. Frames in flight between the receive // thread and the host network are not state: with the threads stopped diff --git a/src/dev/ioc3_eth/tests.rs b/src/dev/ioc3_eth/tests.rs index 12186d75a..ec5a9d167 100644 --- a/src/dev/ioc3_eth/tests.rs +++ b/src/dev/ioc3_eth/tests.rs @@ -816,6 +816,42 @@ fn the_last_reference_released_on_another_thread_stops_the_mac() { assert!(!running.load(Ordering::SeqCst), "the last release stops it"); } +#[test] +fn fuel_phy_mdio_decoder_and_machine_reset() { + let eth = Ioc3Eth::new_fuel(STATION); + assert_eq!(eth.mac(), STATION); + let read = |phy: u32, r: u32| { + eth.write32(MII_CONTROL, MII_READ | (phy << 5) | r); + eth.read32(MII_DATA_R) + }; + assert_eq!(read(1, 2), 0x2000); + assert_eq!(read(1, 3), 0x5c10); + assert_eq!(read(0, 2), 0xffff); + eth.write32(MII_DATA_W, 7); + eth.write32(MII_CONTROL, (1 << 5) | 0x19); + assert_eq!(read(1, 2), 0xffff); + assert_eq!(read(7, 2), 0x2000); + // A wrong-address reset has no effect. + eth.write32(MII_DATA_W, 0x8000); + eth.write32(MII_CONTROL, 1 << 5); + assert_eq!(read(7, 2), 0x2000); + eth.reset(); + assert_eq!(read(1, 3), 0x5c10); + assert_eq!(read(7, 2), 0xffff); + // The MAC soft reset leaves PHY programming intact. + eth.write32(MII_DATA_W, 0x21); + eth.write32(MII_CONTROL, (1 << 5) | 4); + eth.write32(MAC_CONTROL, MCR_RESET); + assert_eq!(read(1, 4), 0x21); + // Forced mode exposes the configured backend cable without waiting. + eth.write32(MII_DATA_W, 0x2100); + eth.write32(MII_CONTROL, 1 << 5); + for up in [false, true] { + eth.set_backend_link(up); + assert_eq!(read(1, 1) & 4 != 0, up); + } +} + /// A guest that does nothing but ACK: the whole host side of a lock-step /// TFTP read (transmit descriptor, NAT thread, built-in server, receive /// thread, receive ring), with the guest's own software left out. Every diff --git a/src/dev/mod.rs b/src/dev/mod.rs index c8e3177b3..a8392b11e 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,4 +60,6 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod dp83843; +pub mod ics1890; pub mod isp12160; From 0c88fe6abbb7f6e594418f1d71324599f44c5353 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 04/20] timekeeper: add the M48T35 clock and NVRAM Fuel stores its clock and nonvolatile configuration in an M48T35 on the IOC3 byte bus. Keep the register overlay and battery-backed bytes together so latching and persistence share one owner. --- src/dev/mod.rs | 1 + src/dev/timekeeper.rs | 701 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 702 insertions(+) create mode 100644 src/dev/timekeeper.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index a8392b11e..d8f7d6b72 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -63,3 +63,4 @@ pub mod z85c30; pub mod dp83843; pub mod ics1890; pub mod isp12160; +pub mod timekeeper; diff --git a/src/dev/timekeeper.rs b/src/dev/timekeeper.rs new file mode 100644 index 000000000..95a2557a5 --- /dev/null +++ b/src/dev/timekeeper.rs @@ -0,0 +1,701 @@ +//! The Fuel M48T35 Timekeeper exposes 32 KiB of byte-wide NVRAM and calendar +//! registers at the end of that window. The guest reads the clock independently +//! of processor byte order, and software can stage clock changes through the +//! control register before committing them. +//! +//! # What is approximated here +//! +//! Host time and a persisted sidecar stand in for the battery-backed clock. +//! The model does not reproduce the physical oscillator or battery behavior. +//! +//! Ported from hw/rtc/m48t35.c in jamesbraid/qemu-sgi-origami. + +use crate::traits::{BusDevice, BusRead8, Device, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +pub const SIZE: usize = 0x8000; +pub mod reg { + pub const CONTROL: usize = 0x7ff8; + pub const SECONDS: usize = 0x7ff9; + pub const MINUTES: usize = 0x7ffa; + pub const HOURS: usize = 0x7ffb; + pub const CLOCK_WRITE: usize = 0x7ffc; + pub const DAY: usize = 0x7ffd; + pub const MONTH: usize = 0x7ffe; + pub const YEAR: usize = 0x7fff; +} +pub const CONTROL: usize = reg::CONTROL; +const CENTURY: i64 = 36525 * 86400; +const MAGIC: &[u8; 8] = b"M48TREF1"; +fn bcd(v: i64) -> u8 { + ((v / 10) * 16 + v % 10) as u8 +} +fn unbcd(v: u8) -> i64 { + i64::from((v >> 4) * 10 + (v & 15)) +} +fn month_days(y: i64, m: i64) -> i64 { + match m { + 2 => 28 + i64::from(y % 4 == 0), + 4 | 6 | 9 | 11 => 30, + _ => 31, + } +} +#[derive(Clone, Copy)] +struct Calendar { + year: i64, + month: i64, + day: i64, + hour: i64, + minute: i64, + second: i64, +} +impl Calendar { + fn seconds(self) -> i64 { + let year = (self.year + (self.month - 1).div_euclid(12)).rem_euclid(100); + let month = (self.month - 1).rem_euclid(12) + 1; + let mut days = year * 365 + (year + 3) / 4 + self.day - 1; + for m in 1..month { + days += month_days(year, m); + } + (days * 86400 + self.hour * 3600 + self.minute * 60 + self.second).rem_euclid(CENTURY) + } + fn from_seconds(t: i64) -> Self { + let t = t.rem_euclid(CENTURY); + let mut days = t / 86400; + let mut year = 0; + while days >= 365 + i64::from(year % 4 == 0) { + days -= 365 + i64::from(year % 4 == 0); + year += 1; + } + let mut month = 1; + while days >= month_days(year, month) { + days -= month_days(year, month); + month += 1; + } + Self { + year, + month, + day: days + 1, + hour: t / 3600 % 24, + minute: t / 60 % 60, + second: t % 60, + } + } +} +fn saved_calendar(image: &[u8]) -> Option { + if image.len() != SIZE { + return None; + } + let values = [ + image[0x7ff9] & 0x7f, + image[0x7ffa], + image[0x7ffb], + image[0x7ffd], + image[0x7ffe], + image[0x7fff], + ]; + let limits = [(0, 59), (0, 59), (0, 23), (1, 31), (1, 12), (0, 99)]; + let valid = values + .iter() + .zip(limits) + .all(|(&v, (lo, hi))| v & 15 <= 9 && v >> 4 <= 9 && (lo..=hi).contains(&unbcd(v))); + if !valid { + return None; + } + Some(Calendar { + second: unbcd(values[0]), + minute: unbcd(values[1]), + hour: unbcd(values[2]), + day: unbcd(values[3]), + month: unbcd(values[4]), + year: unbcd(values[5]), + }) +} +struct State { + cells: Vec, + offset: i64, + stopped: Option, + frozen: Option, + staged: Option<(Calendar, bool)>, + file: Option, + #[cfg(test)] + extra: i64, +} +impl State { + fn running(&self, host: i64) -> i64 { + self.stopped + .unwrap_or_else(|| (host + self.offset).rem_euclid(CENTURY)) + } + fn calendar(&self, host: i64) -> Calendar { + Calendar::from_seconds(self.frozen.unwrap_or_else(|| self.running(host))) + } +} + +/// Fuel's 1968-based clock. Ordinary storage starts blank; the guest owns it. +/// The lower candidate clock addresses are independent RAM, with no aliases. +pub struct Timekeeper { + initial_epoch: i64, + st: Mutex, + clock: Arc i64 + Send + Sync>, +} +impl Timekeeper { + pub fn fuel() -> Result { + let host = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_secs() as i64; + // Gregorian host date, used only to initialize the part's own calendar. + let mut days = host / 86400; + let mut year = 1970; + loop { + let n = 365 + i64::from(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); + if days < n { + break; + } + days -= n; + year += 1; + } + let mut month = 1; + while days >= month_days(year, month) { + days -= month_days(year, month); + month += 1; + } + let t = Self::fuel_at( + year, + month, + days + 1, + host / 3600 % 24, + host / 60 % 60, + host % 60, + )?; + Ok(t) + } + /// Open authoritative saved clock state before applying new-store year limits. + pub fn fuel_with_nvram_file(path: &Path) -> Result { + Self::backed(path, Self::fuel()) + } + fn backed(path: &Path, initial: Result) -> Result { + let saved = match std::fs::read(path) { + Ok(v) => v, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(), + Err(e) => return Err(e.to_string()), + }; + let clock = if saved_calendar(&saved).is_some() { + // This seed is replaced by the authoritative image before exposure. + Self::fuel_at(1968, 1, 1, 0, 0, 0)? + } else { + initial? + }; + clock.set_nvram_file(path)?; + Ok(clock) + } + /// A deterministic initial date; subsequent seconds follow monotonic host time. + pub fn fuel_at( + year: i64, + month: i64, + day: i64, + hour: i64, + minute: i64, + second: i64, + ) -> Result { + if !(1968..=2067).contains(&year) { + return Err("Timekeeper: initial year is outside 1968-2067".into()); + } + if !(1..=12).contains(&month) + || !(1..=month_days(year, month)).contains(&day) + || !(0..24).contains(&hour) + || !(0..60).contains(&minute) + || !(0..60).contains(&second) + { + return Err("Timekeeper: invalid initial calendar".into()); + } + let epoch = Calendar { + year: year - 1968, + month, + day, + hour, + minute, + second, + } + .seconds(); + let at = Instant::now(); + Ok(Self { + initial_epoch: epoch, + st: Mutex::new(State { + cells: vec![0; SIZE], + offset: epoch, + stopped: None, + frozen: None, + staged: None, + file: None, + #[cfg(test)] + extra: 0, + }), + clock: Arc::new(move || at.elapsed().as_secs() as i64), + }) + } + fn host(&self, st: &State) -> i64 { + #[cfg(not(test))] + let _ = st; + (self.clock)() + { + #[cfg(test)] + { + st.extra + } + #[cfg(not(test))] + { + 0 + } + } + } + #[cfg(test)] + fn advance_for_test(&self, seconds: i64) { + self.st.lock().extra += seconds; + } + + fn image(st: &State, host: i64, saved_at: i64) -> (Vec, [u8; 16]) { + let c = Calendar::from_seconds(st.running(host)); + let mut image = st.cells.clone(); + image[CONTROL + 1] = bcd(c.second) | if st.stopped.is_some() { 0x80 } else { 0 }; + image[CONTROL + 2] = bcd(c.minute); + image[CONTROL + 3] = bcd(c.hour); + image[CONTROL + 5] = bcd(c.day); + image[CONTROL + 6] = bcd(c.month); + image[CONTROL + 7] = bcd(c.year); + let mut reference = [0; 16]; + reference[..8].copy_from_slice(MAGIC); + reference[8..].copy_from_slice(&saved_at.to_le_bytes()); + (image, reference) + } + /// Export the exact raw image and companion clock reference, without changing RAM. + pub fn persistence_image_at(&self, saved_at: i64) -> (Vec, [u8; 16]) { + let st = self.st.lock(); + Self::image(&st, self.host(&st), saved_at) + } + /// Restore validated storage. Invalid/unwritten clock cells keep the initial clock. + /// A missing/invalid reference adds no elapsed time, matching the model's policy. + pub fn restore_image_at( + &self, + image: &[u8], + reference: Option<&[u8]>, + now: i64, + ) -> Result<(), String> { + if image.len() != SIZE { + return Err(format!( + "Timekeeper: backing image has {} bytes, expected {SIZE}", + image.len() + )); + } + if reference.is_some_and(|r| r.len() < 16) { + return Err("Timekeeper: clock reference is shorter than 16 bytes".into()); + } + let mut st = self.st.lock(); + st.cells.copy_from_slice(image); + st.frozen = None; + st.staged = None; + st.stopped = None; + if let Some(c) = saved_calendar(image) { + let stopped = image[0x7ff9] & 0x80 != 0; + let elapsed = reference.filter(|r| &r[..8] == MAGIC).map_or(0, |r| { + now.saturating_sub(i64::from_le_bytes(r[8..16].try_into().unwrap())) + .max(0) + }); + let epoch = (c.seconds() + + if stopped { + 0 + } else { + elapsed.rem_euclid(CENTURY) + }) + .rem_euclid(CENTURY); + st.offset = epoch - self.host(&st); + if stopped { + st.stopped = Some(epoch); + } + } else { + st.offset = self.initial_epoch; + crate::dlog!(crate::devlog::LogModule::Rtc, "Timekeeper: unwritten or invalid clock cells; following initial clock"); + } + Ok(()) + } + /// Bind a raw store and its `.clock` companion. Validate both before creating either. + pub fn set_nvram_file(&self, path: &Path) -> Result<(), String> { + let clock_path = PathBuf::from(format!("{}.clock", path.display())); + let image = match std::fs::read(path) { + Ok(v) => Some(v), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + if image.as_ref().is_some_and(|v| v.len() != SIZE) { + return Err("Timekeeper: backing image must contain exactly 32768 bytes".into()); + } + let reference = match std::fs::read(&clock_path) { + Ok(v) => Some(v), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e.to_string()), + }; + if reference.as_ref().is_some_and(|v| v.len() < 16) { + return Err("Timekeeper: clock reference is shorter than 16 bytes".into()); + } + if let Some(image) = image.as_ref() { + self.restore_image_at(image, reference.as_deref(), unix_now())?; + } + use std::io::Write; + if image.is_none() { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .and_then(|mut f| f.write_all(&vec![0; SIZE])) + .map_err(|e| e.to_string())?; + } + if reference.is_none() { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(clock_path) + .and_then(|mut f| f.write_all(&[0; 16])) + .map_err(|e| e.to_string())?; + } + self.st.lock().file = Some(path.to_path_buf()); + Ok(()) + } + pub fn flush_nvram(&self) -> Result<(), String> { + let st = self.st.lock(); + if let Some(path) = &st.file { + let (image, reference) = Self::image(&st, self.host(&st), unix_now()); + std::fs::write(path, image).map_err(|e| e.to_string())?; + use std::io::Write; + std::fs::OpenOptions::new() + .write(true) + .open(format!("{}.clock", path.display())) + .and_then(|mut f| f.write_all(&reference)) + .map_err(|e| e.to_string())?; + } + Ok(()) + } +} +fn unix_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |d| d.as_secs() as i64) +} +impl BusDevice for Timekeeper { + fn read8(&self, addr: u64) -> BusRead8 { + let Ok(a) = usize::try_from(addr) else { + return BusRead8::err(); + }; + if a >= SIZE { + return BusRead8::err(); + } + let st = self.st.lock(); + let c = st.calendar(self.host(&st)); + let data = match a { + reg::SECONDS => bcd(c.second) | if st.stopped.is_some() { 0x80 } else { 0 }, + reg::MINUTES => bcd(c.minute), + reg::HOURS => bcd(c.hour), + reg::DAY => bcd(c.day), + reg::MONTH => bcd(c.month), + reg::YEAR => bcd(c.year), + _ => st.cells[a], + }; + BusRead8 { + status: BUS_OK, + data, + } + } + fn write8(&self, addr: u64, val: u8) -> u32 { + let Ok(a) = usize::try_from(addr) else { + return BUS_ERR; + }; + if a >= SIZE { + return BUS_ERR; + } + let mut st = self.st.lock(); + let host = self.host(&st); + let old_control = st.cells[CONTROL]; + let transfer = a == CONTROL && st.cells[CONTROL] & 0x80 != 0 && val & 0x80 == 0; + if a == CONTROL { + let old = st.cells[a]; + if val & 0x40 != 0 && old & 0x40 == 0 { + st.frozen = Some(st.running(host)); + } else if val & 0x40 == 0 { + st.frozen = None; + } + if val & 0x80 != 0 && old & 0x80 == 0 { + st.staged = Some((st.calendar(host), st.stopped.is_some())); + } else if val & 0x80 == 0 && old & 0x80 != 0 { + if let Some((c, stop)) = st.staged.take() { + let t = c.seconds(); + st.offset = t - host; + st.stopped = if stop { Some(t) } else { None }; + } + } + st.cells[a] = val; + } else if a > CONTROL { + if a == reg::CLOCK_WRITE && st.staged.is_some() { + st.cells[a] = val; + } else if let Some((c, stop)) = &mut st.staged { + match a { + reg::SECONDS => { + c.second = unbcd(val & 0x7f); + *stop = val & 0x80 != 0; + } + reg::MINUTES => c.minute = unbcd(val), + reg::HOURS => c.hour = unbcd(val), + reg::DAY => c.day = unbcd(val), + reg::MONTH => c.month = unbcd(val), + reg::YEAR => c.year = unbcd(val), + _ => {} + } + } else { + crate::dlog!(crate::devlog::LogModule::Rtc, "Timekeeper: clock write at {a:#x} without write enable"); + } + } else { + st.cells[a] = val; + } + if !transfer + && (a < CONTROL + || a == CONTROL && (old_control ^ val) & !0xc0 != 0 + || a == reg::CLOCK_WRITE && st.staged.is_some()) + { + // Ordinary storage writes must not save or replace the clock cells. + if let Some(path) = &st.file { + use std::io::{Seek, SeekFrom, Write}; + let result = + std::fs::OpenOptions::new() + .write(true) + .open(path) + .and_then(|mut f| { + f.seek(SeekFrom::Start(a as u64))?; + f.write_all(&[st.cells[a]]) + }); + if let Err(e) = result { + log::error!("Timekeeper: backing write failed: {e}"); + } + } + } + drop(st); + if transfer { + if let Err(e) = self.flush_nvram() { + log::error!("Timekeeper: clock save failed: {e}"); + } + } + BUS_OK + } +} +impl Resettable for Timekeeper { + fn power_on(&self) { + let mut st = self.st.lock(); + st.frozen = None; + st.staged = None; + st.cells[CONTROL] &= !0xc0; + } +} +impl Device for Timekeeper { + fn start(&self) {} + fn step(&self, _cycles: u64) {} + fn is_running(&self) -> bool { + false + } + fn get_clock(&self) -> u64 { + 0 + } + fn stop(&self) { + if let Err(e) = self.flush_nvram() { + log::error!("Timekeeper: saving failed: {e}"); + } + } +} +impl Drop for Timekeeper { + fn drop(&mut self) { + Device::stop(self); + } +} +#[cfg(test)] +mod tests { + // Cases ported from tests/qtest/sn1-fuel-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + use crate::traits::{BusDevice, Resettable}; + + /// The guest can freeze, stop, stage, and resume the M48T35 calendar. + #[test] + fn clock_control_freezes_stops_and_resets_the_calendar() { + let t = Timekeeper::fuel_at(2067, 12, 31, 23, 59, 59).unwrap(); + t.write8(0x7f8, 0x42); + t.write8(CONTROL as u64, 0x40); + t.advance_for_test(2); + assert_eq!(t.read8(reg::YEAR as u64).data, 0x99); + t.power_on(); + assert_eq!(t.read8(CONTROL as u64).data, 0); + assert_eq!(t.read8(reg::YEAR as u64).data, 0); + assert_eq!(t.read8(reg::MONTH as u64).data, 1); + assert_eq!(t.read8(reg::DAY as u64).data, 1); + assert_eq!(t.read8(0x7f8).data, 0x42); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x89); + t.write8(CONTROL as u64, 0); + t.advance_for_test(60); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x89); + } + + /// A restored guest clock advances by host elapsed time unless its stop bit was saved. + #[test] + fn restored_clock_accounts_for_elapsed_time_unless_stopped() { + for stopped in [false, true] { + let t = Timekeeper::fuel_at(2024, 2, 28, 23, 59, 59).unwrap(); + t.write8(0x7ff0, 0xa5); + if stopped { + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x85); + t.write8(CONTROL as u64, 0); + } + let (image, reference) = t.persistence_image_at(100); + let restored = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + restored.restore_image_at(&image, Some(&reference), 102).unwrap(); + assert_eq!(restored.read8(reg::DAY as u64).data, if stopped { 0x28 } else { 0x29 }); + assert_eq!(restored.read8(reg::SECONDS as u64).data, if stopped { 0x85 } else { 1 }); + assert_eq!(restored.read8(0x7ff0).data, 0xa5); + assert!(restored.restore_image_at(&image[..2048], None, 102).is_err()); + } + } + + /// NVRAM persists across emulator restarts, and malformed backing images are rejected. + #[test] + fn backed_nvram_preserves_saved_state_and_rejects_invalid_images() { + let dir = std::env::current_exe().unwrap().parent().unwrap() + .join(format!("timekeeper-test-{}", std::process::id())); + std::fs::create_dir(&dir).unwrap(); + let path = dir.join("nvram"); + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.set_nvram_file(&path).unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), vec![0; SIZE]); + t.write8(0x7f8, 0x42); + assert_eq!(std::fs::read(&path).unwrap()[0x7f8], 0x42); + t.flush_nvram().unwrap(); + drop(t); + // Saved clock state is authoritative even if the host date is out of range. + let restored = Timekeeper::backed(&path, Timekeeper::fuel_at(2068, 1, 1, 0, 0, 0)).unwrap(); + restored.power_on(); + assert_eq!(restored.read8(0x7f8).data, 0x42); + assert_eq!(restored.read8(reg::YEAR as u64).data, 0x58); + drop(restored); + std::fs::remove_file(dir.join("nvram.clock")).unwrap(); + std::fs::write(&path, [0xa5; 2048]).unwrap(); + assert!(Timekeeper::backed(&path, Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0)).is_err()); + assert_eq!(std::fs::read(&path).unwrap(), vec![0xa5; 2048]); + assert!(!dir.join("nvram.clock").exists()); + std::fs::remove_file(&path).unwrap(); + assert!(Timekeeper::backed(&path, Timekeeper::fuel_at(2068, 1, 1, 0, 0, 0)).is_err()); + assert!(!path.exists()); + std::fs::remove_dir(dir).unwrap(); + } + + const WEEKDAY: u64 = reg::CLOCK_WRITE as u64; + + fn clock_cells(t: &Timekeeper) -> [u8; 7] { + std::array::from_fn(|i| t.read8(reg::SECONDS as u64 + i as u64).data) + } + + fn set_clock(t: &Timekeeper, cells: [u8; 7]) { + t.write8(CONTROL as u64, 0x80); + for (i, v) in cells.into_iter().enumerate() { + t.write8(reg::SECONDS as u64 + i as u64, v); + } + t.write8(CONTROL as u64, 0); + } + + // Cells are seconds, minutes, hours, weekday, day, month, year, all BCD. + + /// The PROM and kernel derive file timestamps from this calendar, so + /// 2024 must have a 29 February and 2026 must not. + #[test] + fn the_calendar_rolls_over_february_in_leap_years_only() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x28, 0x02, 0x56]); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x29, 0x02, 0x56]); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x28, 0x02, 0x58]); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x01, 0x03, 0x58]); + } + + /// The part counts a two-digit year from 1968, so the calendar wraps after + /// 2067; the guest must see year 00 again, not garbage. + #[test] + fn the_year_wraps_from_2067_to_1968() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + set_clock(&t, [0x59, 0x59, 0x23, 0x05, 0x31, 0x12, 0x99]); + assert_eq!(t.read8(reg::YEAR as u64).data, 0x99); + t.advance_for_test(1); + assert_eq!(clock_cells(&t), [0, 0, 0, 0x05, 0x01, 0x01, 0]); + } + + /// A guest reading the clock one byte at a time needs the read bit to + /// freeze every counter at once, or a rollover tears the timestamp. + #[test] + fn the_read_bit_freezes_all_counters_until_it_is_cleared() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + let end_of_century = [0x59, 0x59, 0x23, 0x05, 0x31, 0x12, 0x99]; + set_clock(&t, end_of_century); + t.write8(CONTROL as u64, 0x40); + assert_eq!(t.read8(CONTROL as u64).data, 0x40); + t.advance_for_test(2); + assert_eq!(clock_cells(&t), end_of_century); + t.write8(CONTROL as u64, 0); + assert_eq!(clock_cells(&t), [0x01, 0, 0, 0x05, 0x01, 0x01, 0]); + } + + /// Writes in the write-enable window are staged: the guest sets six + /// counters and the weekday, and nothing but the weekday is visible until + /// the window closes. + #[test] + fn staged_clock_writes_apply_only_when_the_write_bit_clears() { + let t = Timekeeper::fuel_at(2026, 1, 2, 3, 4, 5).unwrap(); + let before = clock_cells(&t); + t.write8(CONTROL as u64, 0x80); + for (i, v) in [0x59u8, 0x58, 0x17, 0x03, 0x15, 0x06, 0x30].into_iter().enumerate() { + t.write8(reg::SECONDS as u64 + i as u64, v); + } + let during = clock_cells(&t); + for i in (0..7).filter(|&i| i != 3) { + assert_eq!(during[i], before[i], "cell {i}"); + } + assert_eq!(t.read8(WEEKDAY).data, 0x03); + t.write8(CONTROL as u64, 0); + assert_eq!(clock_cells(&t), [0x59, 0x58, 0x17, 0x03, 0x15, 0x06, 0x30]); + } + + /// The stop bit is how a guest parks the oscillator while it sets the time; + /// the clock must hold until the guest clears it, then continue from there. + #[test] + fn the_stop_bit_holds_the_seconds_until_it_is_cleared() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x82); + t.write8(CONTROL as u64, 0); + t.advance_for_test(3); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x82); + t.write8(CONTROL as u64, 0x80); + t.write8(reg::SECONDS as u64, 0x02); + t.write8(CONTROL as u64, 0); + t.advance_for_test(1); + assert_eq!(t.read8(reg::SECONDS as u64).data, 0x03); + } + + /// The 32 KiB window ends at the year register; an access past it must + /// fault, and ordinary storage must keep its contents across clock writes. + #[test] + fn accesses_past_the_window_fault_and_ordinary_storage_is_untouched() { + let t = Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap(); + t.write8(0, 0x5a); + t.write8(0x7ff7, 0xa5); + assert_eq!(t.write8(SIZE as u64, 0x69), BUS_ERR); + assert_ne!(t.read8(SIZE as u64).status, BUS_OK); + set_clock(&t, [0, 0, 0, 0x05, 0x01, 0x03, 0x58]); + assert_eq!(t.read8(0).data, 0x5a); + assert_eq!(t.read8(0x7ff7).data, 0xa5); + } +} From 928c793366e918fb4b9d08db01e0a694be92f5df Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:36 +0000 Subject: [PATCH 05/20] ioc3: add the Fuel peripheral population Fuel attaches an M48T35 and has different PCI header policy from the Octane. Carry those choices in the population configuration and wire the timekeeper once so byte-bus accesses need no mutable attachment slot. The Fuel's onboard IOC3 also carries the PS/2 keyboard and mouse controller: OpenBSD configures a keyboard controller through the onboard IOC3 (pci/ioc.c, dev/iockbc.c), and the controller core is the one the Octane already has. Turn the controller on in the Fuel's population, as the Octane's is, and test it by sending a host key through the IOC3 registers: it latches the keyboard interrupt and arrives in the K_RX register, and a mouse command answers from its own port, both before and after a chip reset. --- src/dev/ioc3.rs | 97 ++++++++++++++++++++++++++++++++++++------- src/dev/ioc3_tests.rs | 79 +++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 14 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index f82bbbde5..a17803a9b 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -1,10 +1,11 @@ -//! SGI IOC3 base I/O controller, as the Octane (IP30) fits it. +//! SGI IOC3 base I/O controller, with Octane and Fuel board populations. //! //! One PCI function with one 1 MiB memory BAR. Behind it: two 16550 serial //! ports with a ring-DMA engine each, a PS/2 keyboard and mouse controller, //! a parallel port, a MicroLAN (one-wire) master, the GPIO pins, a DS1685 -//! clock on the byte bus, the Ethernet buffer RAM and, in the register range -//! at 0xf0, the Ethernet MAC (which lives in `ioc3_eth.rs` behind +//! clock on the Octane byte bus or Fuel's flat system Timekeeper, the +//! Ethernet buffer RAM and, in the register range at 0xf0, +//! the Ethernet MAC (which lives in `ioc3_eth.rs` behind //! [`Ioc3EthPort`]). //! //! Behaviour follows the public Linux and OpenBSD drivers and measured guest @@ -60,7 +61,7 @@ use crate::dev::w1::sgi::{self, BoardRecord}; use crate::dev::ns16550::Ns16550; use crate::dev::serial_host::SerialBackend; use crate::irq::{InterruptController, IrqLine}; -use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PciDmaStatus, PCI_INTA}; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PciDmaStatus, PCI_INTA}; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; use crate::traits::{BusDevice, Device, MachineEvent, Resettable, Saveable}; use crate::snapshot::{ @@ -72,6 +73,8 @@ pub const DEVICE_ID: u16 = 0x0003; pub const REVISION: u8 = 0x01; /// PCI subsystem id of the Octane's system board. pub const SUBSYS_IP30_SYSBOARD: u16 = 0xc304; +/// Fuel system-board population. +pub const SUBSYS_IP34_SYSBOARD: u16 = 0xc30a; pub const BAR_SIZE: u32 = 1 << 20; /// The configuration dwords the chip decodes; the rest of the 16 repeat them. const CONFIG_DECODED_DWORDS: usize = 8; @@ -113,6 +116,7 @@ pub mod reg { pub const PAR_ECP_BASE: u32 = 0x20150; pub const SSRAM: u32 = 0x40000; pub const SSRAM_SIZE: u32 = 0x40000; + pub const BYTEBUS_DEV0: u32 = 0x80000; pub const BYTEBUS_DEV1: u32 = 0xa0000; pub const BYTEBUS_DEV2: u32 = 0xc0000; pub const BYTEBUS_SIZE: u32 = 0x20000; @@ -734,6 +738,10 @@ fn int_out_valid(v: u32) -> bool { #[derive(Clone, Debug)] pub struct Ioc3Config { pub subsystem_id: u16, + /// Function-specific PCI header write behavior. + pub pci_policy: PciConfigPolicy<'static>, + /// Reset the PCI header along with the chip on power-on. + pub reset_pci_on_power_on: bool, /// The board's station address, which the one-wire part holds. pub mac: [u8; 6], pub serial: [bool; 2], @@ -763,6 +771,30 @@ impl BoardRecordText { } impl Ioc3Config { + /// Fuel's IOC3 fits the keyboard and mouse controller but no indexed Dallas clock. + pub fn fuel(mac: [u8; 6]) -> Self { + Self { + subsystem_id: SUBSYS_IP34_SYSBOARD, + pci_policy: PciConfigPolicy { + status: 0x0280, + status_w1c_mask: 0, + command_mask: 0xffff, + latency_mask: 0xff, + cache_line_mask: 0, + ..Default::default() + }, + reset_pci_on_power_on: true, + mac, + serial: [true, true], + parallel: true, + keyboard_mouse: true, + rtc: false, + gpio_input_mask: 0, + gpio_input_values: 0, + records: Vec::new(), + } + } + /// The Octane's IOC3 (`octane2` selects the Octane2's board records). /// The fictitious barcodes and ROM serials are the /// marked-synthetic ones (barcode `EMU...`). @@ -774,6 +806,8 @@ impl Ioc3Config { }; Self { subsystem_id: SUBSYS_IP30_SYSBOARD, + pci_policy: PciConfigPolicy::default(), + reset_pci_on_power_on: false, mac, serial: [true, true], parallel: true, @@ -2382,6 +2416,7 @@ impl Shared { /// The IOC3 PCI function. pub struct Ioc3 { sh: Arc, + timekeeper: OnceLock>, /// The serial receive threads and the NVRAM writer, joined in `stop`. threads: Mutex>>, } @@ -2389,22 +2424,26 @@ pub struct Ioc3 { impl Ioc3 { pub fn new(cfg: Ioc3Config) -> Ioc3 { let subsystem = cfg.subsystem_id; + let pci_policy = cfg.pci_policy; let sh = Arc::new_cyclic(|weak: &Weak| { let line = IrqLine::new(Arc::new(KbcSink(weak.clone())), 0); let ps2 = Arc::new(Ps2Controller::new(Some(line))); // Host key and mouse events are accepted from the start (this // only sets a flag); `stop` and `start` follow the chip's. Device::start(&*ps2); - let pci = PciConfig::new( - VENDOR_ID, - DEVICE_ID, - 0xff_0000, - REVISION, - [Bar::Mem(BAR_SIZE), Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused, Bar::Unused], - PCI_INTA, + let bars = [ + Bar::Mem(BAR_SIZE), + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + ]; + let pci = PciConfig::new_with_policy( + VENDOR_ID, DEVICE_ID, 0xff_0000, REVISION, bars, PCI_INTA, pci_policy, ); // The subsystem identity selects what a guest instantiates. - pci.write(11, ((subsystem as u32) << 16) | VENDOR_ID as u32); + pci.set_subsystem_identity(((subsystem as u32) << 16) | VENDOR_ID as u32); Shared { pci, st: Mutex::new(St::new(cfg)), @@ -2429,7 +2468,7 @@ impl Ioc3 { nvram_ready: Condvar::new(), } }); - Ioc3 { sh, threads: Mutex::new(Vec::new()) } + Ioc3 { sh, timekeeper: OnceLock::new(), threads: Mutex::new(Vec::new()) } } /// The Octane's (or Octane2's) onboard IOC3: both serial ports, PS/2, @@ -2438,7 +2477,13 @@ impl Ioc3 { Ioc3::new(Ioc3Config::octane(mac, octane2)) } - /// The timer manager the deadlines are armed on. Call before `start`. + /// Attach Fuel's independently owned flat select-0 system clock. + pub fn attach_timekeeper(&self, clock: Arc) { + assert!(!self.sh.st.lock().cfg.rtc, "indexed Dallas and flat Timekeeper populations conflict"); + assert!(self.timekeeper.set(clock).is_ok(), "IOC3 timekeeper attached more than once"); + } + + /// The timer manager for device deadlines. Set it before start. pub fn set_timer_manager(&self, tm: Arc) { let _ = self.sh.timers.set(tm); } @@ -2545,6 +2590,9 @@ impl Ioc3 { /// Ethernet block is a separate device here and keeps its state: the /// machine resets it on its own. pub fn reset(&self) { + if self.sh.st.lock().cfg.reset_pci_on_power_on { + self.sh.pci.reset(); + } self.sh.with_st(false, |st, _| st.reset()); } @@ -2703,6 +2751,16 @@ impl PciBusDevice for Ioc3 { if width == 4 && a <= reg::HEADER_LAST && a & 3 == 0 { return self.header_dword((a / 4) as usize); } + if (reg::BYTEBUS_DEV0..reg::BYTEBUS_DEV0 + crate::dev::timekeeper::SIZE as u32).contains(&a) { + if let Some(clock) = self.timekeeper.get() { + if width != 1 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC3 Timekeeper {width}-byte read refused"); + return 0; + } + return u32::from(clock.read8(u64::from(a - reg::BYTEBUS_DEV0)).data); + } + } + if (reg::ENET_FIRST..=reg::ENET_LAST).contains(&a) { // Only 32-bit accesses reach the Ethernet block. if width != 4 { @@ -2719,6 +2777,17 @@ impl PciBusDevice for Ioc3 { return; } let a = off ^ (4 - width.min(4) as u32); + if (reg::BYTEBUS_DEV0..reg::BYTEBUS_DEV0 + crate::dev::timekeeper::SIZE as u32).contains(&a) { + if let Some(clock) = self.timekeeper.get() { + if width != 1 { + crate::dlog!(crate::devlog::LogModule::Ioc, "IOC3 Timekeeper {width}-byte write refused"); + return; + } + clock.write8(u64::from(a - reg::BYTEBUS_DEV0), val as u8); + return; + } + } + if (reg::ENET_FIRST..=reg::ENET_LAST).contains(&a) { if width == 4 && a & 3 == 0 { if let Some(e) = self.sh.eth() { diff --git a/src/dev/ioc3_tests.rs b/src/dev/ioc3_tests.rs index ec7f1bfe5..825907170 100644 --- a/src/dev/ioc3_tests.rs +++ b/src/dev/ioc3_tests.rs @@ -1636,3 +1636,82 @@ fn a_short_buffer_ram_is_refused() { chip.insert("ssram".into(), toml::Value::Array(Vec::new())); assert!(c.load_state(&v).unwrap_err().contains("ssram")); } + +/// The Octane keeps the IOC3's PCI header across a chip reset; only the +/// Fuel population resets it along with the chip. +#[test] +fn an_octane_chip_reset_keeps_the_pci_header() { + let r = Rig::new(); + let cfg = r.c.config(); + cfg.write(1, 0xffff_0146); + cfg.write(3, 0xffff_ffff); + r.c.reset(); + assert_eq!(cfg.read(1), 0x0200_0146); + assert_eq!(cfg.read(3), 0xffff); +} + +/// Fuel's IOC3 has its own header write policy and resets the header with +/// the chip, reaches the flat Timekeeper on byte-bus device 0, leaves the +/// Dallas clock ports unfitted, and has no PHY reset on GPIO 5. +#[test] +fn the_fuel_population_keeps_its_clock_and_header_policy_across_reset() { + let r = Rig::with(Ioc3::new(Ioc3Config::fuel(MAC))); + let clock = Arc::new(crate::dev::timekeeper::Timekeeper::fuel_at(2026, 1, 1, 0, 0, 0).unwrap()); + r.c.attach_timekeeper(clock.clone()); + let cfg = r.c.config(); + cfg.write(1, 0xffff_0146); + cfg.write(3, 0xffff_ffff); + assert_eq!(cfg.read(1), 0x0280_0146); + assert_eq!(cfg.read(3), 0x0000_ff00); + let base = reg::BYTEBUS_DEV0; + r.w8(base + 0x7f8, 0xa5); + r.c.reset(); + clock.power_on(); + assert_eq!(r.r8(base + 0x7fff), 0x58); + assert_eq!(r.r8(base + 0x7f8), 0xa5); + assert_eq!(cfg.read(1), 0x0280_0000); + assert_eq!(cfg.read(3), 0); + // Fuel selects the flat clock, leaving indexed Dallas ports unfitted. + r.w8(reg::BYTEBUS_DEV1, 0x40); + r.w8(reg::BYTEBUS_DEV2, 0x55); + assert_eq!(r.r8(reg::BYTEBUS_DEV2), 0); + r.c.attach_eth(crate::dev::ioc3_eth::Ioc3Eth::new_fuel(MAC)); + use crate::dev::ioc3_eth::{bits::MII_READ, reg::{MII_CONTROL, MII_DATA_R, MII_DATA_W}}; + r.w32(MII_DATA_W, 7); + r.w32(MII_CONTROL, (1 << 5) | 0x19); + r.w32(reg::PIN_CONTROL_SET, 1 << 5); + r.w32(reg::PIN_REG_FIRST + 5 * 4, 0); + r.w32(reg::PIN_DATA, 1 << 5); + r.w32(MII_CONTROL, MII_READ | (7 << 5) | 3); + assert_eq!(r.r32(MII_DATA_R), 0x5c10, "Fuel GPIO5 has no PHY reset connection"); +} + +/// Fuel's IOC3 fits the keyboard and mouse controller, as the Octane's does +/// (OpenBSD configures it through the onboard IOC3's keyboard controller). A +/// key pushed from the host arrives in K_RX through the IOC3 registers and +/// latches the keyboard interrupt, and a mouse report arrives in M_RX; both +/// survive a chip reset. +#[test] +fn the_fuel_ioc3_delivers_keyboard_and_mouse_traffic_across_reset() { + let r = Rig::with(Ioc3::new(Ioc3Config::fuel(MAC))); + for _ in 0..2 { + r.c.reset(); + r.c.ps2().power_on(); + r.w32(K_TX, 0xff); + let v = r.r32(K_RX); + assert_ne!(v & VALID[2], 0, "the keyboard answers a reset"); + assert_eq!((v >> 16) & 0xff, 0xfa, "with an ACK"); + r.w32(M_TX, 0xf2); + let v = r.r32(M_RX); + assert_ne!(v & (1 << 27), 0, "the mouse answers from its own port"); + // Enable scanning, then inject a key from the host. + r.w32(K_TX, 0xf4); + while r.r32(K_RX) & VALID[2] != 0 {} + r.w32(reg::INT_STATUS, int::KEYBOARD); + r.c.ps2().push_kb(winit::keyboard::KeyCode::KeyA, true); + assert_ne!(r.r32(reg::INT_STATUS) & int::KEYBOARD, 0, "a key latches the interrupt"); + let v = r.r32(K_RX); + assert_ne!(v & VALID[2], 0, "and arrives in K_RX"); + assert_eq!(v & (1 << 27), 0, "from the keyboard"); + } +} From f468d8741d526bca20759b191b4fbf36aea8332a Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 06/20] sflash: add the AMD command set Fuel boot flash uses AMD unlock, program and erase commands. Keep that state machine separate from array storage and bus decode so its command handling can serve each fitted flash array. The Intel and AMD command sets need the same rules for seeding and writing flash images, so share the file storage while each command engine keeps its own command state. --- HACKING.md | 3 + debug.md | 3 +- src/dev/sflash.rs | 39 ++--- src/dev/sflash/amd.rs | 313 ++++++++++++++++++++++++++++++++++++++ src/dev/sflash/storage.rs | 62 ++++++++ src/devlog.rs | 8 +- 6 files changed, 400 insertions(+), 28 deletions(-) create mode 100644 src/dev/sflash/amd.rs create mode 100644 src/dev/sflash/storage.rs diff --git a/HACKING.md b/HACKING.md index 879ca4b5b..b58c83c76 100644 --- a/HACKING.md +++ b/HACKING.md @@ -104,6 +104,9 @@ 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. +Flash command engines share `FlashBacking` file storage while retaining +their own command state. + ## 2. Data path and endianness **Word-transparent architecture.** Host `u32`/`u64` values are bit diff --git a/debug.md b/debug.md index 934f6aa79..5c7775204 100644 --- a/debug.md +++ b/debug.md @@ -593,9 +593,10 @@ Per-module logging; all of it needs a developer build to produce output. | `log file ` | Send a module's log to a file | Modules: `net hpc3 seeq hal2 mc rex3 mips ioc scsi pdma vino dcb vc2 cmap -xmap bt445 scc ps2 rtc eeprom l1i l1d l2c`. Mask categories: `pdma` — +xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock`. Mask categories: `pdma` — `hal enet scsi`; `mips` — `insn tlb mem fpu cp0`; `l1i`/`l1d`/`l2c` — `hit miss op`; every module also takes `on`/`all`, `off`/`none` or a hex mask. +`bedrock` covers the Fuel's devices on its Bedrock hub. `IRIS_DEBUG_LOG` or `[debug] debug_log` set the same thing at startup. ### Performance and locks diff --git a/src/dev/sflash.rs b/src/dev/sflash.rs index 6d0e75ea6..5d35bfe91 100644 --- a/src/dev/sflash.rs +++ b/src/dev/sflash.rs @@ -30,8 +30,7 @@ //! [`Bridge::attach_flash`]: crate::dev::bridge::Bridge::attach_flash use std::cell::Cell; -use std::fs::{File, OpenOptions}; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io; use std::path::Path; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -40,6 +39,10 @@ use parking_lot::Mutex; use crate::snapshot::{field_u8, StateTable}; use crate::traits::{BusDevice, BusRead16, Saveable, BUS_ERR, BUS_OK}; +pub(crate) mod storage; +pub(crate) mod amd; +use self::storage::FlashBacking; + /// The Octane's part: 1 MiB. pub const FLASH_SIZE: usize = 1 << 20; /// Erase block size. @@ -78,7 +81,7 @@ struct State { data: Vec, mode: Mode, status: u8, - file: Option, + backing: Option, } pub struct CfiFlash { @@ -114,7 +117,7 @@ impl CfiFlash { assert!(image.len() <= size, "image must fit the part"); let mut data = vec![0xff; size]; data[..image.len()].copy_from_slice(image); - Self { st: Mutex::new(State { data, mode: Mode::Array, status: SR_READY, file: None }), gen: AtomicU64::new(0) } + Self { st: Mutex::new(State { data, mode: Mode::Array, status: SR_READY, backing: None }), gen: AtomicU64::new(0) } } /// A reset: back to read-array mode with the status clear. The array @@ -132,23 +135,10 @@ impl CfiFlash { /// untouched, since it is probably some other file. pub fn with_file(image: &[u8], size: usize, path: impl AsRef) -> io::Result { let flash = Self::new(image, size); - let path = path.as_ref(); - let mut file = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(path)?; - { - let mut st = flash.st.lock(); - let len = file.metadata()?.len(); - if len == size as u64 { - file.read_exact(&mut st.data)?; - } else if len == 0 { - file.write_all(&st.data)?; - } else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("flash file {} is {len} bytes, expected {size} (or empty); not overwriting it", path.display()), - )); - } - st.file = Some(file); - } + let mut st = flash.st.lock(); + let backing = FlashBacking::open(path, &mut st.data)?; + st.backing = Some(backing); + drop(st); Ok(flash) } @@ -168,10 +158,9 @@ impl CfiFlash { } impl State { - fn sync(&self, off: usize, len: usize) { - if let Some(mut f) = self.file.as_ref() { - // `&File` seeks and writes without needing `&mut self`. - let res = f.seek(SeekFrom::Start(off as u64)).and_then(|_| f.write_all(&self.data[off..off + len])); + fn sync(&mut self, off: usize, len: usize) { + if let Some(backing) = self.backing.as_mut() { + let res = backing.write_range(off, &self.data[off..off + len]); if let Err(e) = res { // The guest sees a completed cycle; the file is only the backing store. log::warn!("flash: backing file write at {off:#x} failed: {e}"); diff --git a/src/dev/sflash/amd.rs b/src/dev/sflash/amd.rs new file mode 100644 index 000000000..41c4cdff9 --- /dev/null +++ b/src/dev/sflash/amd.rs @@ -0,0 +1,313 @@ +//! The AMD flash command set: unlock, word program, sector erase and CFI +//! query state, over storage supplied by the caller. +//! +//! Array bytes and bus translation belong to the enclosing device. Fuel uses +//! this 8 MiB command engine for each boot-flash array. The CFI data and timing +//! are compatibility values; they do not identify the fitted flash chip. +//! Buffered writes, protection, bypass and erase suspend are absent. +//! +//! Ported from MAME's intelfsh.cpp AMD command path, revision +//! 2ee9b1e30299b122b131cab0bd7912abc2b095ba (BSD-3-Clause). + +// Copyright (c) Aaron Giles + +pub const SECTOR_SIZE: usize = 64 * 1024; +const SECTORS: usize = 128; +pub const PART_SIZE: usize = SECTOR_SIZE * SECTORS; +pub const ERASE_SETUP_NS: u64 = 50_000; +pub const ERASE_NS: u64 = 512_000_000; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mode { + Array, + Unlock1, + Unlock2, + Program, + Erase1, + Erase2, + Erase3, + Query, + Identify, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Effect { + None, + Program { offset: usize, value: u16 }, +} + +pub struct AmdCore { + mode: Mode, + selected: [bool; SECTORS], + /// Remaining time includes the sector-selection interval. + remaining_ns: u64, + setup_ns: u64, + toggle: bool, + failed: bool, +} + +impl Default for AmdCore { + fn default() -> Self { + Self { + mode: Mode::Array, + selected: [false; SECTORS], + remaining_ns: 0, + setup_ns: 0, + toggle: false, + failed: false, + } + } +} + +impl AmdCore { + pub fn reset(&mut self) { + *self = Self::default(); + } + pub fn busy(&self) -> bool { + self.remaining_ns != 0 || self.failed + } + pub fn selecting(&self) -> bool { + self.setup_ns != 0 + } + pub fn array_reads(&self) -> bool { + !self.busy() && self.mode != Mode::Query && self.mode != Mode::Identify + } + /// None selects ordinary backing bytes. Polling never advances time. + pub fn read16(&mut self, offset: usize) -> Option { + if self.busy() { + let selected = self.selected[offset / SECTOR_SIZE]; + self.toggle = !self.toggle; + return Some( + (if self.toggle { + 0x40 | if selected { 4 } else { 0 } + } else { + 0 + }) | if self.setup_ns == 0 { 8 } else { 0 } + | if self.failed { 0x20 } else { 0 }, + ); + } + match self.mode { + Mode::Query => Some(query(offset / 2) as u16), + // IDs remain unspecified until a part/profile is selected. + Mode::Identify => Some(0), + _ => None, + } + } + pub fn write16(&mut self, offset: usize, value: u16) -> Result { + if offset & 1 != 0 || offset >= PART_SIZE { + return Err(()); + } + let cmd = value as u8; + let word = offset / 2; + if self.busy() { + if (self.failed || self.setup_ns != 0) && cmd == 0xf0 { + self.reset(); + } else if self.setup_ns != 0 && cmd == 0x30 { + self.selected[offset / SECTOR_SIZE] = true; + self.setup_ns = ERASE_SETUP_NS; + self.remaining_ns = ERASE_SETUP_NS + ERASE_NS; + } else if self.setup_ns != 0 { + self.reset(); + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: sector selection cancelled by command {cmd:#x} at word offset {word:#x}" + ); + return Err(()); + } else if cmd != 0xf0 { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: unsupported command {cmd:#x} while erasing at word offset {word:#x}" + ); + return Err(()); + } + return Ok(Effect::None); + } + // The pending program data cycle consumes every bit, including F0. + if self.mode == Mode::Program { + self.mode = Mode::Array; + return Ok(Effect::Program { offset, value }); + } + if cmd == 0xf0 { + self.reset(); + return Ok(Effect::None); + } + let next = match (self.mode, word, cmd) { + (Mode::Array, 0x55, 0x98) => Mode::Query, + (Mode::Array, 0x555, 0xaa) => Mode::Unlock1, + (Mode::Unlock1, 0x2aa, 0x55) => Mode::Unlock2, + (Mode::Unlock2, 0x555, 0xa0) => Mode::Program, + (Mode::Unlock2, 0x555, 0x80) => Mode::Erase1, + (Mode::Unlock2, 0x555, 0x90) => Mode::Identify, + (Mode::Erase1, 0x555, 0xaa) => Mode::Erase2, + (Mode::Erase2, 0x2aa, 0x55) => Mode::Erase3, + (Mode::Erase3, _, 0x30) => { + self.selected[offset / SECTOR_SIZE] = true; + self.setup_ns = ERASE_SETUP_NS; + self.remaining_ns = ERASE_SETUP_NS + ERASE_NS; + Mode::Array + } + _ => { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "amd-flash: unsupported cycle {value:#06x} at byte offset {offset:#x} in {:?}", + self.mode + ); + self.mode = Mode::Array; + return Err(()); + } + }; + self.mode = next; + Ok(Effect::None) + } + /// The owner persists and applies selected sectors before calling finish. + pub fn advance_ns(&mut self, ns: u64) -> bool { + if self.remaining_ns == 0 || self.failed { + return false; + } + self.setup_ns = self.setup_ns.saturating_sub(ns); + self.remaining_ns = self.remaining_ns.saturating_sub(ns); + self.remaining_ns == 0 + } + pub fn selected_sectors(&self) -> impl Iterator + '_ { + self.selected + .iter() + .enumerate() + .filter_map(|(i, selected)| selected.then_some(i)) + } + pub fn finish_erase(&mut self, success: bool) { + if success { + self.reset(); + } else { + self.failed = true; + } + } +} + +// Generic model response; voltage/timing do not identify Fuel's fitted part. +// Field positions/units: AMD publication 21444 rev E+2, pp14-15. +// Region encoding also follows Renode AMDCFIFlash.cs (MIT), revision +// 5448105bc7875aa6c5305761d97a371dc640dd7f. No Renode mutation code is used. +fn query(index: usize) -> u8 { + const PROFILE: [u8; 33] = [ + b'Q', b'R', b'Y', 2, 0, 0, 0, 0, 0, 0, 0, 0x27, 0x36, 0, 0, 3, 0, 9, 0, 4, 0, 4, 0, 23, 1, + 0, 0, 0, 1, 0x7f, 0, 0, 1, + ]; + index + .checked_sub(0x10) + .and_then(|i| PROFILE.get(i)) + .copied() + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + // Cases ported from upstream QEMU's tests/qtest/pflash-cfi02-test.c. + use super::*; + + // Word addresses from the command sequences, as byte offsets on the 16-bit bus. + const UNLOCK0: usize = 0x555 * 2; + const UNLOCK1: usize = 0x2aa * 2; + const CFI: usize = 0x55 * 2; + + fn cmd(f: &mut AmdCore, offset: usize, value: u16) -> Effect { + f.write16(offset, value).unwrap() + } + + fn unlock(f: &mut AmdCore) { + cmd(f, UNLOCK0, 0xaa); + cmd(f, UNLOCK1, 0x55); + } + + fn start_sector_erase(f: &mut AmdCore, offset: usize) { + unlock(f); + cmd(f, UNLOCK0, 0x80); + unlock(f); + cmd(f, offset, 0x30); + } + + /// The guest's erase loop polls DQ3, DQ6 and DQ7; get the phases wrong and + /// firmware either times out or reads the sector before it is blank. + #[test] + fn a_sector_erase_reports_toggle_status_until_it_completes() { + let mut f = AmdCore::default(); + let sector = 5 * SECTOR_SIZE; + start_sector_erase(&mut f, sector); + assert!(f.busy() && f.selecting()); + assert!(!f.array_reads()); + // DQ3 clear: still inside the window where more sectors may be added. + let first = f.read16(sector).unwrap(); + assert_eq!(first & 0x08, 0); + assert_eq!(first & 0x80, 0); + f.advance_ns(ERASE_SETUP_NS); + assert!(!f.selecting()); + let a = f.read16(sector).unwrap(); + let b = f.read16(sector).unwrap(); + assert_eq!(a & 0x08, 0x08); + assert_eq!(a & 0x80, 0); + assert_eq!(a & 0x40, !b & 0x40, "DQ6 toggles on successive reads"); + assert!(f.advance_ns(ERASE_NS)); + assert_eq!(f.selected_sectors().collect::>(), [5]); + f.finish_erase(true); + assert!(f.array_reads()); + assert_eq!(f.read16(sector), None); + } + + /// Firmware erases several sectors with one command; each further 0x30 + /// inside the selection window must join the set, not start a second erase. + #[test] + fn extra_sectors_join_a_pending_erase_inside_the_selection_window() { + let mut f = AmdCore::default(); + start_sector_erase(&mut f, 0); + cmd(&mut f, 3 * SECTOR_SIZE, 0x30); + assert_eq!(f.selected_sectors().collect::>(), [0, 3]); + // The window restarts on each selection. + f.advance_ns(ERASE_SETUP_NS - 1); + assert!(f.selecting()); + // A non-erase command in the window cancels the whole operation. + assert!(f.write16(0, 0x55).is_err()); + assert!(!f.busy()); + assert_eq!(f.selected_sectors().count(), 0); + } + + /// Reset (F0) is how firmware backs out of a half-finished command or + /// leaves CFI mode, so it must always return the part to array reads. + #[test] + fn a_reset_command_abandons_a_partial_unlock_and_leaves_cfi_mode() { + let mut f = AmdCore::default(); + cmd(&mut f, UNLOCK0, 0xaa); + cmd(&mut f, 0, 0xf0); + // The unlock is forgotten: a program setup now is not a valid cycle. + assert!(f.write16(UNLOCK1, 0x55).is_err()); + + cmd(&mut f, CFI, 0x98); + assert!(!f.array_reads()); + cmd(&mut f, 0, 0xf0); + assert!(f.array_reads()); + assert_eq!(f.read16(0x10 * 2), None); + } + + /// Firmware that probes the part reads the CFI "QRY" signature and the + /// device-size exponent to learn how much flash it has. + #[test] + fn the_cfi_query_returns_the_qry_signature_and_the_size_exponent() { + let mut f = AmdCore::default(); + cmd(&mut f, CFI, 0x98); + let q = |f: &mut AmdCore, word: usize| f.read16(word * 2).unwrap(); + assert_eq!([q(&mut f, 0x10), q(&mut f, 0x11), q(&mut f, 0x12)], [b'Q' as u16, b'R' as u16, b'Y' as u16]); + assert_eq!(1usize << q(&mut f, 0x27), PART_SIZE); + } + + /// Word program is the unlock sequence, A0, then one data cycle; the data + /// cycle goes to the owner as an effect and is not decoded as a command. + #[test] + fn a_program_sequence_hands_the_data_word_to_the_owner() { + let mut f = AmdCore::default(); + unlock(&mut f); + cmd(&mut f, UNLOCK0, 0xa0); + // 0xf0 as data must not be taken for a reset. + assert_eq!(cmd(&mut f, 0x1234 * 2, 0xa5f0), Effect::Program { offset: 0x1234 * 2, value: 0xa5f0 }); + assert!(f.array_reads()); + // Odd byte offsets are not word cycles. + assert!(f.write16(UNLOCK0 + 1, 0xaa).is_err()); + } +} diff --git a/src/dev/sflash/storage.rs b/src/dev/sflash/storage.rs new file mode 100644 index 000000000..815e1aa6d --- /dev/null +++ b/src/dev/sflash/storage.rs @@ -0,0 +1,62 @@ +//! Exact-size file backing shared by flash command-set models. + +use std::fs::{File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +pub(crate) struct FlashBacking { + file: File, + size: usize, +} + +impl FlashBacking { + #[cfg(test)] + pub(crate) fn from_file(file: File, size: usize) -> Self { + Self { file, size } + } + + /// Open a flash store, loading its bytes or seeding a new empty file. + /// A non-empty file must match the supplied array size and is left intact + /// when its size is refused. + pub(crate) fn open(path: impl AsRef, data: &mut [u8]) -> io::Result { + let path = path.as_ref(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path)?; + let len = file.metadata()?.len(); + if len == data.len() as u64 { + file.read_exact(data)?; + } else if len == 0 { + file.write_all(data)?; + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "flash file {} is {len} bytes, expected {} (or empty); not overwriting it", + path.display(), + data.len() + ), + )); + } + Ok(Self { + file, + size: data.len(), + }) + } + + /// Write one byte range through to the backing file. + pub(crate) fn write_range(&mut self, offset: usize, bytes: &[u8]) -> io::Result<()> { + if offset > self.size || bytes.len() > self.size - offset { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "flash backing range is outside the array", + )); + } + self.file.seek(SeekFrom::Start(offset as u64))?; + self.file.write_all(bytes)?; + self.file.flush() + } +} diff --git a/src/devlog.rs b/src/devlog.rs index d082e7ba1..b9ee4f6fa 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -55,6 +55,7 @@ pub enum LogModule { /// The Octane's VPro (Odyssey) graphics board: guest accesses the /// model does not support. Odyssey = 28, + Bedrock = 29, } // Mask bits shared by L1i / L1d / L2c @@ -63,7 +64,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 29; + pub const COUNT: usize = 30; pub fn from_str(s: &str) -> Option { match s { @@ -96,6 +97,7 @@ impl LogModule { "gr2" => Some(Self::Gr2), "rad1" => Some(Self::Rad1), "odyssey" => Some(Self::Odyssey), + "bedrock" => Some(Self::Bedrock), _ => None, } } @@ -131,6 +133,7 @@ impl LogModule { Self::Gr2 => "gr2", Self::Rad1 => "rad1", Self::Odyssey => "odyssey", + Self::Bedrock => "bedrock", } } @@ -141,7 +144,7 @@ impl LogModule { Self::Vino, Self::Dcb, Self::Vc2, Self::Cmap, Self::Xmap, Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, - Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, + Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, ] } @@ -283,6 +286,7 @@ impl DevLog { ModuleLog::new(), // Gr2 ModuleLog::new(), // Rad1 ModuleLog::new(), // Odyssey + ModuleLog::new(), // Bedrock ], } } From 8fb1a6cef4c0aafd8d9f6173749eb7f19ad05f31 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:03:25 +0000 Subject: [PATCH 07/20] cpu: send uncached byte and halfword stores with the whole register Fuel PROM 6.210's memory test fails when uncached SB and SH reach the bus carrying only their low bits. Its L1 console shows: mtest_bank_main failed: Walking Address RSLT mtest_bank_mai FAIL diag_rc = 43 Walking Address In both memory banks the expected halfwords 0xa55a, 0x5aa5, 0x9999 and 0x6666 read back from the ECC check-bit view as 0x0000. The PROM stores each check word with SH from a register whose low 16 bits are zero; the word sits higher in the register, at a position that depends on the tested address. With the whole register visible to the ECC view and the same PROM configuration it reports ALL DIAGS PASSED and enables all 512 MiB. Add BusDevice::write_narrow_from, whose default stores the architectural byte or halfword exactly as write8/write16 do, and call it for every uncached SB and SH. Ordinary devices therefore still see the low bits of the register, and only a device that models a path observing the other lanes overrides it; Bedrock's ECC view does, in its own commit. Which lanes real hardware drives for a narrow store is undefined. Debug writes keep the ordinary-width path, and the JIT callouts pass the same source register. The default adds one virtual call to a path that already makes one. --- HACKING.md | 4 ++ src/cpu/mips_exec.rs | 23 +++++-- src/cpu/mips_exec_test.rs | 140 ++++++++++++++++++++++++++++++++++++++ src/traits.rs | 14 ++++ 4 files changed, 174 insertions(+), 7 deletions(-) diff --git a/HACKING.md b/HACKING.md index b58c83c76..a74f8e56e 100644 --- a/HACKING.md +++ b/HACKING.md @@ -384,6 +384,10 @@ consumer thread never takes `rec`. monomorphised over it, so every CPU is in every binary with no per-model branch on the hot path. `Machine::new` picks one from `[machine] cpu`. +- Every uncached SB/SH goes to the bus through `BusDevice::write_narrow_from` + with the whole source register: the byte or halfword is the low bits, the + other lanes are undefined by hardware, and only Bedrock's ECC view reads + them. - With `opcodefusion` (implied by `lightning`), the interpreter executes common pairs (branch + NOP, LUI + ORI/ADDIU, address + load/store) as one dispatch. diff --git a/src/cpu/mips_exec.rs b/src/cpu/mips_exec.rs index 194c46938..df2db98b1 100644 --- a/src/cpu/mips_exec.rs +++ b/src/cpu/mips_exec.rs @@ -1571,20 +1571,21 @@ unsafe extern "C" fn jit_read64(ctx: *mut core::ffi::c_void #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write8(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *exec_from_core::(ctx) }; - let val = val as u8; // mask to the real width ourselves — see write8_fn's doc comment on why the FFI parameter is u64 + // The whole register, as the interpreter's SB passes it: `write_data` + // narrows it where it stores, and an uncached store hands it to the bus + // (`BusDevice::write_narrow_from`). #[cfg(feature = "jitv2_lockstep")] - { return exec.lockstep_jit_write::<1>(va, val as u64); } + { return exec.lockstep_jit_write::<1>(va, val); } #[cfg(not(feature = "jitv2_lockstep"))] - exec.write_data::<1>(va, val as u64) + exec.write_data::<1>(va, val) } #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write16(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { let exec = unsafe { &mut *exec_from_core::(ctx) }; - let val = val as u16; // mask to the real width ourselves — see write16_fn's doc comment #[cfg(feature = "jitv2_lockstep")] - { return exec.lockstep_jit_write::<2>(va, val as u64); } + { return exec.lockstep_jit_write::<2>(va, val); } #[cfg(not(feature = "jitv2_lockstep"))] - exec.write_data::<2>(va, val as u64) + exec.write_data::<2>(va, val) } #[cfg(feature = "jitv2")] unsafe extern "C" fn jit_write32(ctx: *mut core::ffi::c_void, va: u64, val: u64) -> u32 { @@ -5457,7 +5458,15 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", if !DEBUG && mips_log(MIPS_LOG_MEM) { dlog_dev!(LogModule::Mips, "Uncached Write{}: PC={:016x} VA={:016x} PA={:016x} Val={:016x}", SIZE*8, self.core.pc, virt_addr, phys_addr, val); } - let ws = if SIZE == 1 { + let ws = if !DEBUG && SIZE <= 2 { + // The bus sees the whole source register: Fuel PROM 6.210's + // Walking Address test reads its check words back from + // Bedrock's ECC view, and the word sits elsewhere in the + // register than the addressed lanes. Ordinary devices take + // the architectural low bits. Debug writes carry just the + // value they name, so they keep the ordinary-width path. + self.sysad.write_narrow_from(phys_addr, SIZE, val) + } else if SIZE == 1 { self.sysad.write8(phys_addr, val as u8) } else if SIZE == 2 { self.sysad.write16(phys_addr, val as u16) diff --git a/src/cpu/mips_exec_test.rs b/src/cpu/mips_exec_test.rs index f0f798c8f..49d591187 100644 --- a/src/cpu/mips_exec_test.rs +++ b/src/cpu/mips_exec_test.rs @@ -13,12 +13,18 @@ mod tests { // Mock Memory Interface pub struct MockMemory { pub data: Mutex>, + /// `write8`/`write16` calls as (width, address, value). + pub narrow_writes: Mutex>, + /// `write_narrow_from` calls as (width, address, source register). + pub narrow_from_writes: Mutex>, } impl MockMemory { pub fn new() -> Self { Self { data: Mutex::new(HashMap::new()), + narrow_writes: Mutex::new(Vec::new()), + narrow_from_writes: Mutex::new(Vec::new()), } } @@ -67,6 +73,7 @@ mod tests { } fn write8(&self, addr: u64, val: u8) -> u32 { + self.narrow_writes.lock().unwrap().push((1, addr, val as u64)); self.set_byte(addr as u64, val); BUS_OK } @@ -82,6 +89,7 @@ mod tests { fn write16(&self, addr: u64, val: u16) -> u32 { let aligned_addr = (addr & !1) as u64; + self.narrow_writes.lock().unwrap().push((2, aligned_addr, val as u64)); let bytes = val.to_be_bytes(); for i in 0..2 { self.set_byte(aligned_addr + i as u64, bytes[i]); @@ -111,6 +119,12 @@ mod tests { BUS_OK } + fn write_narrow_from(&self, addr: u64, size: usize, gpr: u64) -> u32 { + self.narrow_from_writes.lock().unwrap().push((size, addr, gpr)); + // Same architectural bytes as the default. + if size == 1 { self.write8(addr, gpr as u8) } else { self.write16(addr, gpr as u16) } + } + // This whole file predates jitv2 and no test here calls // `install_jit_hooks` — every `create_executor*` helper below // explicitly disables both the real dispatch gate @@ -575,6 +589,132 @@ mod tests { assert_eq!(mem.get_byte(0x100B), 0xEF); } + /// Fuel PROM memory diagnostics need the whole source register on the ECC + /// view, so every CPU hands an uncached SB or SH to + /// `BusDevice::write_narrow_from` with it. The default of that call stores + /// the architectural byte or halfword, which is all an ordinary device + /// sees, and the addressed lanes hold it either way. + #[test] + fn uncached_sb_sh_carry_the_whole_source_register() { + fn check>>() { + let mem = Arc::new(MockMemory::new()); + let bus: Arc = mem.clone(); + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip22_bus::memory_map(false))); + let mut exec = MipsExecutor::::new( + bus, + PassthroughTlb::default(), + &cfg, + ); + disable_jitv2(&mut exec); + + let base = 0x2000u64; + let source = 0xFEDC_BA98_7654_3210u64; + let original = 0xAABB_CCDD_EEFF_0011u64; + exec.core.write_gpr(2, base); + exec.core.write_gpr(1, source); + + for (size, op, offsets) in [(1usize, OP_SB, vec![0u64, 1, 2, 3, 4, 5, 6, 7]), + (2usize, OP_SH, vec![0u64, 2, 4, 6])] { + for offset in offsets { + mem.set_double(base, original); + mem.narrow_writes.lock().unwrap().clear(); + mem.narrow_from_writes.lock().unwrap().clear(); + let instr = make_i(op, 2, 1, offset as u16); + assert_eq!(exec.exec(instr), EXEC_COMPLETE); + + let addr = base + offset; + assert_eq!( + *mem.narrow_from_writes.lock().unwrap(), + vec![(size, addr, source)], + "SB/SH source register at byte offset {offset}", + ); + + let first = if size == 1 { source as u8 } else { (source as u16 >> 8) as u8 }; + let last = if size == 1 { first } else { source as u8 }; + assert_eq!(mem.get_byte(addr), first, "stored leading byte at offset {offset}"); + if size == 2 { + assert_eq!(mem.get_byte(addr + 1), last, "stored trailing byte at offset {offset}"); + } + let original_bytes = original.to_be_bytes(); + for lane in 0..8u64 { + if lane < offset || lane >= offset + size as u64 { + assert_eq!(mem.get_byte(base + lane), original_bytes[lane as usize], + "inactive byte lane {lane} at subword offset {offset}"); + } + } + } + } + + // A zero source register still takes the same path and clears + // only the addressed lanes. + exec.core.write_gpr(0, u64::MAX); + mem.set_double(base, original); + mem.narrow_from_writes.lock().unwrap().clear(); + exec.core.write_gpr(2, base + 7); + assert_eq!(exec.exec(make_i(OP_SB, 2, 0, 0)), EXEC_COMPLETE); + assert_eq!(mem.get_byte(base + 7), 0); + assert_eq!(mem.narrow_from_writes.lock().unwrap().len(), 1); + + mem.set_double(base, original); + exec.core.write_gpr(2, base + 6); + assert_eq!(exec.exec(make_i(OP_SH, 2, 0, 0)), EXEC_COMPLETE); + assert_eq!(mem.get_byte(base + 6), 0); + assert_eq!(mem.get_byte(base + 7), 0); + assert_eq!(mem.narrow_from_writes.lock().unwrap().len(), 2); + + mem.narrow_writes.lock().unwrap().clear(); + mem.narrow_from_writes.lock().unwrap().clear(); + assert_eq!(exec.debug_write(base + 1, source, 1, u64::MAX), EXEC_COMPLETE); + assert_eq!(*mem.narrow_writes.lock().unwrap(), vec![(1, base + 1, source as u8 as u64)]); + assert!(mem.narrow_from_writes.lock().unwrap().is_empty(), "debug writes keep the ordinary-width path"); + } + + check::(); + check::(); + check::(); + check::(); + } + + /// JIT store callouts carry the same source register as the interpreter. + #[test] + #[cfg(feature = "jitv2")] + fn jit_subword_hooks_carry_the_source_register() { + fn check>>() { + let mem = Arc::new(MockMemory::new()); + let bus: Arc = mem.clone(); + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip22_bus::memory_map(false))); + let mut exec = MipsExecutor::::new( + bus, + PassthroughTlb::default(), + &cfg, + ); + disable_jitv2(&mut exec); + exec.install_jit_hooks(); + + let base = 0x2000u64; + let source = 0xFEDC_BA98_7654_3210u64; + let write8 = exec.core.write8_fn; + let write16 = exec.core.write16_fn; + let core = &mut exec.core as *mut crate::cpu::mips_core::MipsCore; + // JIT callouts receive the core pointer biased by the shared + // ABI constant; core_from_arg removes this before recovery. + let ctx = core.cast::() + .wrapping_add(crate::cpu::jitv2::codegen::CALLOUT_CORE_BIAS as usize) + .cast(); + unsafe { + write8(ctx, base, source); + write16(ctx, base + 2, source); + } + assert_eq!( + *mem.narrow_from_writes.lock().unwrap(), + vec![(1, base, source), (2, base + 2, source)], + ); + } + + check::(); + check::(); + } + #[test] fn test_load_store_pc_advances() { // Regression test: every load/store dispatch-target handler must diff --git a/src/traits.rs b/src/traits.rs index 604cb65c2..a0f322836 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -223,6 +223,20 @@ pub trait BusDevice: Send + Sync { BUS_OK } + /// An uncached SB (`size` 1) or SH (`size` 2) to `addr`, with the whole + /// source register `gpr` for a bus that can see the lanes the store does + /// not select. The CPU calls this instead of `write8`/`write16` for an + /// uncached SB or SH. + /// + /// The architectural byte or halfword is the low bits of `gpr`, which is + /// all the default delivers. Which other lanes a real processor drives for + /// a narrow store is undefined, so only a device that models a path + /// observing them (Bedrock's memory ECC view) overrides this, and a bus + /// that forwards accesses must forward this call too. + fn write_narrow_from(&self, addr: u64, size: usize, gpr: u64) -> u32 { + if size == 1 { self.write8(addr, gpr as u8) } else { self.write16(addr, gpr as u16) } + } + /// JIT v2 (clanker/jitv2/jit-v2-design.md §2.4): pointer to the generation counter /// for the 4KB physical page containing `addr`, or null if this device doesn't /// back JIT-compilable memory (MMIO, etc). RAM devices return one counter per From 22cfff665c86c7d38b4029eb1c7c00bba64dd16f Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 05:16:10 -0700 Subject: [PATCH 08/20] cpu: carry the R10000 family's uncached attribute through mapped accesses The R10000 drives a two-bit uncached attribute on the system bus. For an unmapped xkphys access it is address bits 58:57; for a mapped reference it comes from the TLB's uncached-attribute field, which EntryLo0/1 bits 63:62 load (R10000 User's Manual 007-2490-001). EntryLo already keeps those bits on the family, and a TLB entry holds them, but the entry's physical base dropped them, so a mapped uncached page always reached attribute zero. Carry them to physical bits 58:57 only for pages the model's C table decodes as uncached, so cached mappings are unchanged. The TLB already holds that table for its shadow entries; reading it there, and recomputing each entry's base when a table is installed, avoids a second decode of C. An entry with no attribute bits set, the usual case, skips the C table. The attribute reaches the bus only on a board that decodes physical bits 58:57. The Octane, Indigo2 and O2 mask the physical address to 40, 32 and 32 bits before the bus, which folds it away, and a test shows that at the executor. The R4400 and R5000 never store the bits. Tests cover the attribute and the board masks, and the C table (a value the model decodes as cached carries nothing; a table installed after the entry was written decides). The uncached-attribute comments cite NEC U10278EJ4V0UM, the fourth edition of the VR10000/VR12000 user's manual, section 11.3, by document number; the manual itself is not added to the repository. --- src/cpu/mips_exec_test.rs | 56 +++++++++++++++++++++++++++++++++++++++ src/cpu/mips_tlb.rs | 43 ++++++++++++++++++++++++++---- src/cpu/mips_tlb_test.rs | 49 ++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 5 deletions(-) diff --git a/src/cpu/mips_exec_test.rs b/src/cpu/mips_exec_test.rs index 49d591187..99c8e78f2 100644 --- a/src/cpu/mips_exec_test.rs +++ b/src/cpu/mips_exec_test.rs @@ -4282,6 +4282,62 @@ mod tests { check::(0xF_FFFF_FFFF); } + /// A mapped uncached page's attribute (EntryLo 63:62, carried to physical + /// bits 58:57) reaches the bus on a board that decodes them, and is folded + /// away on the Octane (40 bits), Indigo2 and O2 (32), which is what keeps + /// those machines unchanged. + #[test] + fn mapped_uncached_attribute_is_folded_by_the_boards_physical_mask() { + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + use crate::cpu::mips_tlb::MipsTlb; + const VA: u64 = 0x4000_0000; + + fn translate>>( + phys_mask: u64, alias: Option<(u64, u64, u64)>, pfn: u64, + ) -> u64 { + let bus: Arc = Arc::new(MockMemory::new()); + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip22_bus::memory_map(false))); + let mut exec = Box::new(MipsExecutor::::new( + bus, MipsTlb::default(), &cfg)); + disable_jitv2(&mut exec); + exec.install_status_cb(); + let dmtc0 = |exec: &mut Box>, rd: u32, value: u64| { + exec.core.write_gpr(1, value); + let op = (OP_COP0 << 26) | (RS_DMTC0 << 21) | (1 << 16) | (rd << 11); + assert_eq!(exec.exec(op), EXEC_COMPLETE); + }; + let status = (exec.core.cp0_status & !crate::cpu::mips_core::STATUS_ERL) | STATUS_KX; + dmtc0(&mut exec, 12, status as u64); + dmtc0(&mut exec, 10, VA); + dmtc0(&mut exec, 0, 0); + dmtc0(&mut exec, 5, 0); + // C=2 (uncached), valid, dirty, attribute 1 in bits 63:62. + dmtc0(&mut exec, 2, (1 << 62) | (pfn << 6) | (2 << 3) | (1 << 2) | (1 << 1)); + dmtc0(&mut exec, 3, 0); + assert_eq!(exec.exec((OP_COP0 << 26) | (RS_TLB << 21) | FUNCT_TLBWI), EXEC_COMPLETE); + exec.phys_mask = phys_mask; + exec.phys_alias = alias; + exec.debug_translate(VA).phys + } + + type R10K = crate::cpu::mips_cache_shadow::R10000ShadowCache; + type R14K = crate::cpu::mips_cache_shadow::R12000ShadowCache; + const PA: u64 = 0x0d10_0000; + assert_eq!(translate::(u64::MAX, None, PA >> 12), (1 << 57) | PA, + "no mask: the attribute reaches the bus"); + for (name, mask) in [("Octane", (1u64 << 40) - 1), ("Indigo2/O2", u32::MAX as u64)] { + assert_eq!(translate::(mask, None, PA >> 12), PA, "{name} folds it away"); + } + assert_eq!(translate::(u64::MAX, None, PA >> 12), PA, + "a model without the attribute never stores it"); + }) + .unwrap() + .join() + .unwrap(); + } + /// The Indy, Indigo2 and O2 decode 32 physical bits, and IRIX relies on /// it: on the IP28 the kernel stack's wired entry carries PTE software /// bits as PA[34:33]. `phys_mask` must fold them away before the bus. diff --git a/src/cpu/mips_tlb.rs b/src/cpu/mips_tlb.rs index 3eaed8e8c..ff504582f 100644 --- a/src/cpu/mips_tlb.rs +++ b/src/cpu/mips_tlb.rs @@ -147,6 +147,23 @@ pub enum AccessType { Debug = 3, } +/// The physical base a TLB entry's EntryLo maps. A model that keeps the +/// R10000 family's uncached attribute (EntryLo bits 63:62, see +/// `CpuModel::R10K_FAMILY`) has it carried to physical bits 58:57 +/// for an uncached page, where an unmapped xkphys access carries it; other +/// models never store those bits. For a mapped reference the R10000 takes the +/// two-bit uncached attribute from the TLB's uncached-attribute field, loaded +/// from EntryLo0/1 bits 63:62 (NEC U10278EJ4V0UM, VR10000/VR12000 user's +/// manual, 4th edition, section 11.3; R10000 User's Manual 007-2490-001). +/// +/// Whether the page is uncached is the caller's decode of C through the model's +/// table (`MipsTlb::c_attr`), not another decode here. +#[inline] +fn pfn_base(lo: u64, pfn_mask: u64, uncached: bool) -> u64 { + let attribute = (((lo >> 62) & 3) << 57) & 0u64.wrapping_sub(uncached as u64); + ((lo << 6) & pfn_mask) | attribute +} + /// TLB Entry structure matching R4000 TLB format #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TlbEntry { @@ -1008,6 +1025,21 @@ impl Default for MipsTlb { } } +impl MipsTlb { + /// `pfn_base` for both halves of an entry, deciding "uncached" with the + /// model's C table. + #[inline] + fn pfn_bases(&self, lo: &[u64; 2], pfn_mask: u64) -> [u64; 2] { + // No attribute bits set, the usual case: nothing to carry, and the C + // table need not be read. + if (lo[0] | lo[1]) >> 62 == 0 { + return [(lo[0] << 6) & pfn_mask, (lo[1] << 6) & pfn_mask]; + } + let uncached = |lo: u64| self.c_attr[((lo >> 3) & 7) as usize] == CacheAttr::Uncached; + [pfn_base(lo[0], pfn_mask, uncached(lo[0])), pfn_base(lo[1], pfn_mask, uncached(lo[1]))] + } +} + impl Tlb for MipsTlb { #[inline] fn translate(&mut self, virt_addr: u64, asid: u8, access_type: AccessType) -> TlbResult { @@ -1177,10 +1209,7 @@ impl Tlb for MipsTlb { entry.vpn_hi64 = entry.entry_hi & entry.vcmp64; entry.offset_mask = (1u64 << entry.selector_bit_shift) - 1; let pfn_mask = 0xFF_FFFF_FFFF_000 & !entry.offset_mask; - entry.pfn_base = [ - (entry.entry_lo[0] << 6) & pfn_mask, - (entry.entry_lo[1] << 6) & pfn_mask, - ]; + entry.pfn_base = self.pfn_bases(&entry.entry_lo, pfn_mask); self.irregular_mask |= mask & (mask + 1) != 0; if self.invalidate_on_write { self.invalidate_overlapped(index, &entry); @@ -1322,6 +1351,10 @@ impl Tlb for MipsTlb { fn set_cache_attrs(&mut self, table: [CacheAttr; 8]) { self.c_attr = table; for i in 0..self.num_entries { + // pfn_base carries the attribute for pages the new table decodes + // as uncached, so it follows the table. + let pfn_mask = 0xFF_FFFF_FFFF_000 & !self.entries[i].offset_mask; + self.entries[i].pfn_base = self.pfn_bases(&self.entries[i].entry_lo, pfn_mask); self.shadow[i] = ShadowEntry::from_entry(&self.entries[i], &self.c_attr); } } @@ -1380,7 +1413,7 @@ impl Tlb for MipsTlb { vcmp64, vpn_hi64: entry_hi & vcmp64, offset_mask, - pfn_base: [(lo[0] << 6) & pfn_mask, (lo[1] << 6) & pfn_mask], + pfn_base: self.pfn_bases(&lo, pfn_mask), }; } } diff --git a/src/cpu/mips_tlb_test.rs b/src/cpu/mips_tlb_test.rs index be121f291..65ca06067 100644 --- a/src/cpu/mips_tlb_test.rs +++ b/src/cpu/mips_tlb_test.rs @@ -535,3 +535,52 @@ fn tlb_write_inside_a_larger_entry_count_is_kept() { assert_eq!(r10000.read(63).entry_hi, entry.entry_hi, "...and its last slot"); } } + +/// A mapped uncached page carries EntryLo's uncached attribute (bits 63:62) +/// to the bus as physical bits 58:57; a cached page never does, whatever those bits +/// hold, and no attribute leaves the PFN alone. "Uncached" is the model's C +/// table, so a model that decodes a C value differently, or a table installed +/// after the entry was written, changes the answer. +#[test] +fn only_uncached_pages_carry_the_uncached_attribute() { + use super::{MipsTlb, Tlb, TlbEntry}; + use crate::cpu::mips_exec::CacheAttr; + // MipsTlb is large; give the one instance room, as its neighbours do. + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + let pfn = 0x0d100u64 << 6; + let base = |tlb: &MipsTlb| tlb.read(0).pfn_base; + let write = |tlb: &mut MipsTlb, lo0: u64, lo1: u64| { + let mut entry = TlbEntry::new(); + entry.entry_hi = 0xC000_0000_0004_0000; + entry.entry_lo = [lo0, lo1]; + tlb.write(0, entry); + }; + let mut tlb = Box::new(MipsTlb::new(64)); + // The default table: C=2 and the reserved values are uncached. + for (cca, expect) in [(2u64, 1u64 << 57), (6, 1 << 57), (3, 0), (5, 0)] { + let lo = (1 << 62) | pfn | (cca << 3) | 0x6; + write(&mut tlb, lo, lo); + assert_eq!(base(&tlb), [0x0d10_0000 | expect; 2], "cca {cca}"); + } + write(&mut tlb, pfn | (2 << 3), 0); + assert_eq!(base(&tlb)[0], 0x0d10_0000, "no attribute leaves the PFN alone"); + + // The R10000 table reads C=7 (uncached accelerated) as uncached and + // 5 as cacheable; a model that read 7 as cacheable carries nothing. + let mut table = [CacheAttr::Uncached; 8]; + table[3] = CacheAttr::Cacheable; + table[5] = CacheAttr::CacheableCoherent; + tlb.set_cache_attrs(table); + let lo = (1 << 62) | pfn | (7 << 3) | 0x6; + write(&mut tlb, lo, lo); + assert_eq!(base(&tlb), [0x0d10_0000 | 1 << 57; 2], "C=7 uncached under this table"); + table[7] = CacheAttr::Cacheable; + tlb.set_cache_attrs(table); + assert_eq!(base(&tlb), [0x0d10_0000; 2], "the table, installed later, decides"); + }) + .unwrap() + .join() + .unwrap(); +} From 46513a601927e4eeb49965e3d435d1d3cc99c93f Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 09/20] bedrock_md: add the Fuel memory controller Bedrock memory configuration changes the RAM decode. Keep ordinary CPU and DMA memory on Physical directly, divert diagnostic pages through MD, and update their generation state when memory configuration or ECC changes. The ECC view overrides write_narrow_from to take an uncached SH's check word from the whole source register, at the position Fuel PROM 6.210's memory test uses. Every other view stores the architectural low bits. --- src/dev/bedrock_md.rs | 1713 ++++++++++++++++++++++++++++++++++++++++ src/dev/bedrock_mem.rs | 311 ++++++++ src/dev/mod.rs | 2 + 3 files changed, 2026 insertions(+) create mode 100644 src/dev/bedrock_md.rs create mode 100644 src/dev/bedrock_mem.rs diff --git a/src/dev/bedrock_md.rs b/src/dev/bedrock_md.rs new file mode 100644 index 000000000..b5ef5936b --- /dev/null +++ b/src/dev/bedrock_md.rs @@ -0,0 +1,1713 @@ +//! Bedrock's memory and directory controller: bank configuration, memory +//! self-test, ECC, scrubbing and the directory and fetch-operation views. +//! +//! Memory is held by `bedrock_mem::BedrockMemory`. CPU coherence requests +//! and hardware protection enforcement are absent; protection registers +//! retain their words. Known unsupported writes are counted and ignored, +//! and unmapped accesses return a bus error. +//! +//! Plain register words, directory entries and reference counters, +//! protection entries and injected ECC check bytes live in sparse maps. The +//! guest touches a few of the millions of possible entries, and an entry it +//! never wrote reads as its default, so a dense table would only cost memory. +//! +//! Ported from hw/misc/sgi-bedrock-md.c in jamesbraid/qemu-sgi-origami. + +use super::bedrock_mem::{BedrockMemory, BANK_SPAN, FITTED_BANK}; +use crate::traits::*; +use parking_lot::Mutex; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Weak}; + + +pub mod reg { + pub const CELL: u64 = 0x0; + pub const CONFIG: u64 = 0x8; + pub const ARBITRATION: u64 = 0x10; + pub const MIGRATION_CONFIG: u64 = 0x18; + /// Outgoing reply queue sizes: read-only, so a write changes nothing. + pub const REPLY_QUEUE_SIZE: u64 = 0x60; + pub const PERF_SELECT0: u64 = 0x10000; + pub const PERF_SELECT1: u64 = 0x10008; + pub const PERF_COUNTER0: u64 = 0x10010; + pub const PERF_COUNTER1: u64 = 0x10018; + pub const REFRESH: u64 = 0x20000; + /// Flash, UART and Synergy timing on the local "junk" bus. + pub const JUNK_BUS_TIMING: u64 = 0x20008; + pub const SELFTEST_COMMAND: u64 = 0x20030; + pub const SELFTEST_DATA: u64 = 0x20038; + pub const SELFTEST_ERROR: u64 = 0x20040; + pub const SELFTEST_STATUS: u64 = 0x20048; + pub const ISSUE_DEBUG: u64 = 0x20060; + pub const DIRECTORY_CONFIG: u64 = 0x40000; + pub const DIRECTORY_ERROR: u64 = 0x40010; + /// Reads the directory error record and clears it. + pub const DIRECTORY_ERROR_ALIAS: u64 = 0x40018; + pub const MIGRATION_DIFFERENCE: u64 = 0x40040; + pub const MIGRATION_VALUE: u64 = 0x40048; + /// Outgoing request queue sizes; the model keeps them fixed. + pub const REQUEST_QUEUE_SIZE: u64 = 0x40050; + pub const DIRECTORY_DEBUG: u64 = 0x40060; + pub const ECC_CONFIG: u64 = 0x60000; + pub const MEMORY_ERROR: u64 = 0x60010; + /// Reads the memory error record and clears it. + pub const MEMORY_ERROR_ALIAS: u64 = 0x60018; + pub const MEMORY_DEBUG: u64 = 0x60040; + + /// Reset sizes: 12 entries in each header field and 24 in each data field. + pub const REPLY_QUEUE_SIZES: u64 = 0x0018_1818_0c0c_0c00; + /// Reset sizes: four requests for each of the three queues. + pub const REQUEST_QUEUE_SIZES: u64 = 0x0404_0400; +} + +const CONFIG_MASK: u64 = 0xfbfbfbfb | (3 << 32) | (15 << 44) | (63 << 48) | (7 << 56) | (3 << 60); +const ECC_COLUMNS: [u8; 72] = [ + 0x13, 0x23, 0x43, 0x83, 0x2f, 0xf1, 0x0d, 0x07, 0xd0, 0x70, 0x4f, 0xf8, 0x61, 0x62, 0x64, 0x68, + 0x1c, 0x2c, 0x4c, 0x8c, 0x15, 0x25, 0x45, 0x85, 0x19, 0x29, 0x49, 0x89, 0x1a, 0x2a, 0x4a, 0x8a, + 0x51, 0x52, 0x54, 0x58, 0x91, 0x92, 0x94, 0x98, 0xa1, 0xa2, 0xa4, 0xa8, 0x31, 0x32, 0x34, 0x38, + 0x16, 0x26, 0x46, 0x86, 0x1f, 0xf2, 0x0b, 0x0e, 0xb0, 0xe0, 0x8f, 0xf4, 0xc1, 0xc2, 0xc4, 0xc8, + 1, 2, 4, 8, 16, 32, 64, 128, +]; +pub fn ecc_compute(mut value: u64) -> u8 { + let mut code = 0; + while value != 0 { + code ^= ECC_COLUMNS[value.trailing_zeros() as usize]; + value &= value - 1; + } + code +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MemoryError { + Correctable, + Uncorrectable, +} +type ErrorCallback = Arc; + +struct State { + cell: u64, + config: u64, + refresh: u64, + directory_config: u64, + ecc_config: u64, + storage: HashMap, + directory: HashMap, + counters: HashMap, + protection: HashMap, + compat: HashMap, + ecc: HashMap, + admitted: [u16; 2], + memory_error: u64, + directory_error: u64, + marked_directory: HashSet, + clock_ns: u64, + clock_anchor: Option, + selftest_deadline: Option, + selftest_result: bool, + selftest_status: u64, + selftest_error: u64, +} +impl Default for State { + fn default() -> Self { + Self { + cell: u64::MAX, + config: 0, + refresh: 0, + directory_config: 0, + ecc_config: 0, + storage: HashMap::new(), + directory: HashMap::new(), + counters: HashMap::new(), + protection: HashMap::new(), + compat: HashMap::new(), + ecc: HashMap::new(), + admitted: [0; 2], + memory_error: 0, + directory_error: 0, + marked_directory: HashSet::new(), + clock_ns: 0, + clock_anchor: None, + selftest_deadline: None, + selftest_result: false, + selftest_status: 0, + selftest_error: 0, + } + } +} + +pub struct BedrockMd { + memory: BedrockMemory, + clock: Option, + // Ordinary RAM uses Physical's bank views. Diagnostic metadata, repair + // and fetch operations retain the state lock; direct bank traffic has + // the same concurrency contract as the other machines' RAM. + state: Mutex, + // CPU-side metadata writes and resets serialize snapshot/publication. + // RAM/DMA reads and writes may remove metadata without taking this lock. + remapping: Mutex<()>, + callback: Mutex>, + unsupported: AtomicU64, +} +#[derive(Clone, Copy)] +enum View { + Registers, + Memory, + Uncached, + Scrub, + Directory, + Fetchop, + Ecc, +} +struct MdBus { + md: Weak, + view: View, + // External endpoints may outlive MD. Keep their generation pointers valid. + // The endpoint owned by Physical itself needs no extra reference. + _memory: Option>, + #[cfg(feature = "jitv2")] + _generations: Arc, +} + +impl BedrockMd { + pub fn new() -> Arc { + Self::build(None) + } + pub fn with_clock(clock: crate::dev::heart::Clock) -> Arc { + Self::build(Some(clock)) + } + fn build(clock: Option) -> Arc { + let md = Arc::new(Self { + memory: BedrockMemory::new(), + clock, + state: Mutex::new(State::default()), + remapping: Mutex::new(()), + callback: Mutex::new(None), + unsupported: AtomicU64::new(0), + }); + md.memory.set_slow_view(md.view(View::Memory)); + md.rebuild_memory(); + md + } + pub fn register_locks(self: &Arc) { + let weak = Arc::downgrade(self); + crate::locks::register_lock_fn("bedrock-md::state", move || { + weak.upgrade().is_some_and(|md| md.state.is_locked()) + }); + let weak = Arc::downgrade(self); + crate::locks::register_lock_fn("bedrock-md::remapping", move || { + weak.upgrade().is_some_and(|md| md.remapping.is_locked()) + }); + let weak = Arc::downgrade(self); + crate::locks::register_lock_fn("bedrock-md::callback", move || { + weak.upgrade().is_some_and(|md| md.callback.is_locked()) + }); + } + pub fn physical(&self) -> &Arc { + self.memory.physical() + } + fn view(self: &Arc, view: View) -> Arc { + Arc::new(MdBus { + md: Arc::downgrade(self), + view, + #[cfg(feature = "jitv2")] + _generations: self.memory.generations(), + _memory: if matches!(view, View::Uncached) { + Some(self.physical().clone()) + } else { + None + }, + }) + } + pub fn md_bus(self: &Arc) -> Arc { + self.view(View::Registers) + } + pub fn directory_bus(self: &Arc) -> Arc { + self.view(View::Directory) + } + pub fn fetchop_bus(self: &Arc) -> Arc { + self.view(View::Fetchop) + } + pub fn ecc_bus(self: &Arc) -> Arc { + self.view(View::Ecc) + } + /// DMA uses the same Physical entries as the CPU, without accessing + /// ppmem's CPU-owned bitmap. Hardware protection remains unsupported. + pub fn dma_view(self: &Arc) -> Arc { + Arc::new(DmaView(self.clone())) + } + + fn mapping(s: &State) -> (u64, BTreeSet) { + let pages = s.ecc.keys().map(|key| key / (4096 / 8)) + .chain(s.marked_directory.iter().map(|key| key / (4096 / 128))) + .collect(); + (s.config, pages) + } + + /// Reinstall RAM after the board sets Physical's default. Call on the + /// CPU thread or with the CPU stopped, before installing board overlays. + /// Ordinary CPU config/diagnostic writes rebuild their changed mappings + /// automatically. This also releases pages whose metadata DMA consumed. + pub fn rebuild_memory(&self) { + let _remapping = self.remapping.lock(); + let (config, pages) = Self::mapping(&self.state.lock()); + self.memory.remap(config, pages, true); + } + /// The guest stored `tag` in a primary data cache tag: the PROM building + /// its stack in cache lines, or ending that. While they stand the window + /// answers from a page of its own on every path and stays out of the + /// direct window. See [`BedrockMemory::startup_tag`]. Called on the CPU + /// thread, outside the cache's locks. + pub fn startup_tag_store(&self, tag: u32) { + let Some(live) = self.memory.startup_tag(tag) else { return }; + let _remapping = self.remapping.lock(); + if self.memory.set_startup(live) { + let (config, pages) = Self::mapping(&self.state.lock()); + self.memory.remap(config, pages, true); + } + } + /// The stable logical-physical counters used by both compiled stores + /// and SysAd page claims. Their storage outlives the CPU via its bus. + #[cfg(feature = "jitv2")] + pub(crate) fn generation_base(&self) -> *mut AtomicU64 { + self.memory.generations().base() + } + pub fn on_error(&self, callback: ErrorCallback) { + *self.callback.lock() = Some(callback); + } + /// Explicit elapsed time for a manually clocked controller. + pub fn advance_ns(&self, elapsed: u64) { + let mut s = self.state.lock(); + Self::advance(&mut s, elapsed); + } + fn advance(s: &mut State, elapsed: u64) { + s.clock_ns = s.clock_ns.saturating_add(elapsed); + if s.selftest_deadline.is_some_and(|end| s.clock_ns >= end) { + s.selftest_deadline = None; + s.selftest_status = if s.selftest_result { 3 } else { 2 }; + s.selftest_error = if s.selftest_result { 0 } else { 1 << 48 }; + } + } + fn catch_up(s: &mut State, now: u64) { + if let Some(anchor) = s.clock_anchor { + s.clock_anchor = Some(anchor.max(now)); + Self::advance(s, now.saturating_sub(anchor)); + } + } + pub fn start_clock(&self) { + if let Some(clock) = &self.clock { + let now = clock(); + self.state.lock().clock_anchor.get_or_insert(now); + } + } + pub fn stop_clock(&self) { + if let Some(clock) = &self.clock { + let now = clock(); + let mut s = self.state.lock(); + Self::catch_up(&mut s, now); + s.clock_anchor = None; + } + } + /// Normal MD memory semantics without re-entering the cached Physical map. + pub fn memory_view(self: &Arc) -> Arc { + self.view(View::Memory) + } + pub fn uncached_view(self: &Arc) -> Arc { + self.view(View::Uncached) + } + pub fn ecc_scrub_bus(self: &Arc) -> Arc { + self.view(View::Scrub) + } + pub fn current_cell(&self) -> u64 { + self.state.lock().cell + } + pub fn unsupported(&self) -> u64 { + self.unsupported.load(Ordering::Relaxed) + } + /// Count and log an access the model does not implement; the caller + /// answers a bus error. + fn refuse(&self, what: &str, at: u64) -> Option { + self.unsupported.fetch_add(1, Ordering::Relaxed); + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock MD refused {what} access at {at:#x}"); + None + } + /// Called on the CPU thread or with the CPU stopped. + pub fn soft_reset(&self) { + let _remapping = self.remapping.lock(); + let mut s = self.state.lock(); + s.cell = u64::MAX; + s.storage.clear(); + s.ecc_config = 0; + s.memory_error = 0; + s.directory_error = 0; + s.ecc.clear(); + s.marked_directory.clear(); + s.selftest_deadline = None; + s.selftest_status = 0; + s.selftest_error = 0; + s.refresh &= (1 << 63) | 0xfff; + let (config, pages) = Self::mapping(&s); + drop(s); + self.memory.remap(config, pages, false); + } + fn access(&self, view: View, addr: u64, bytes: u64, write: Option) -> Option { + if ![1, 2, 4, 8].contains(&bytes) || addr % bytes != 0 { + return self.refuse("access", addr); + } + // Only CPU register/configuration and diagnostic writes can add + // metadata. Keep their remap off DMA threads: ordinary RAM, scrub + // and fetch operations only remove entries, leaving safe slow pages + // behind until a CPU-side rebuild. These write views, like HEART's + // MEMCFG, are CPU-thread-only while the machine runs. + let publish = write.is_some() && match view { + View::Registers => matches!(addr, reg::CONFIG | reg::SELFTEST_COMMAND), + View::Directory | View::Ecc => true, + _ => false, + }; + let remapping = publish.then(|| self.remapping.lock()); + let mut event = Vec::new(); + // Sample outside state: even an injected clock must not call out under + // the memory lock. Only register observations need the self-test timer. + let now = if matches!(view, View::Registers) { + self.clock.as_ref().map(|clock| clock()) + } else { + None + }; + let (result, snapshot) = { + let mut s = self.state.lock(); + if let Some(now) = now { + Self::catch_up(&mut s, now); + } + let result = match view { + View::Registers => self.register(&mut s, addr, bytes, write), + View::Directory => self.directory(&mut s, addr, bytes, write), + View::Ecc => self.ecc(&mut s, addr, bytes, write), + View::Scrub => self.scrub(&mut s, addr, bytes, write, &mut event), + View::Uncached => { + let result = self.ram(&mut s, addr, bytes, write, &mut event); + if write.is_none() && event.contains(&MemoryError::Uncorrectable) { + None + } else { + result + } + } + View::Fetchop => self.fetchop(&mut s, addr, bytes, write, &mut event), + View::Memory => self.ram(&mut s, addr, bytes, write, &mut event), + }; + (result, publish.then(|| Self::mapping(&s))) + }; + if let Some((config, pages)) = snapshot { + self.memory.remap(config, pages, false); + } + drop(remapping); + if !event.is_empty() { + let callback = self.callback.lock().clone(); + if let Some(callback) = callback { + for kind in event { + callback(kind); + } + } + } + result + } + fn register(&self, s: &mut State, at: u64, bytes: u64, write: Option) -> Option { + if bytes != 8 { + return self.refuse("register", at); + } + let old = match at { + reg::CELL => s.cell, + reg::CONFIG => s.config, + reg::REPLY_QUEUE_SIZE => reg::REPLY_QUEUE_SIZES, + reg::REFRESH => s.refresh, + reg::SELFTEST_ERROR => s.selftest_error, + reg::SELFTEST_STATUS => s.selftest_status, + reg::DIRECTORY_CONFIG => s.directory_config, + reg::ECC_CONFIG => s.ecc_config, + reg::REQUEST_QUEUE_SIZE => reg::REQUEST_QUEUE_SIZES, + reg::MEMORY_ERROR | reg::MEMORY_ERROR_ALIAS => s.memory_error, + reg::DIRECTORY_ERROR | reg::DIRECTORY_ERROR_ALIAS => s.directory_error, + _ if storage_mask(at).is_some() => *s.storage.get(&at).unwrap_or(&0), + _ if idle(at) => 0, + _ => return self.refuse("register", at), + }; + if let Some(value) = write { + match at { + reg::SELFTEST_COMMAND => return self.selftest(s, value), + reg::CELL => s.cell = value, + reg::CONFIG => { + s.config = value & CONFIG_MASK; + } + reg::REFRESH => s.refresh = value & ((1 << 63) | 0xfff), + reg::DIRECTORY_CONFIG => s.directory_config = value & 3, + reg::ECC_CONFIG => s.ecc_config = value & 1, + _ if storage_mask(at).is_some() => { + s.storage.insert(at, value & storage_mask(at).unwrap()); + } + _ => { + self.unsupported.fetch_add(1, Ordering::Relaxed); + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock MD ignored register write at {at:#x}: {value:#x}"); + } + } + } else if at == reg::MEMORY_ERROR_ALIAS { + s.memory_error = 0; + } else if at == reg::DIRECTORY_ERROR_ALIAS { + s.directory_error = 0; + } + Some(old) + } + fn directory(&self, s: &mut State, at: u64, bytes: u64, write: Option) -> Option { + if bytes != 8 || at >= 1 << 30 { + return self.refuse("directory", at); + } + let software = at / (BANK_SPAN / 8); + let bank = software ^ ((s.config >> 32) & 3); + if at & 0x7c8 == 0x408 { + if bank >= 2 { + return Some(0); + } + let within = + ((((at % (BANK_SPAN / 8)) >> 11) << 14) | (((at >> 4) & 3) << 7)) % FITTED_BANK; + let key = ((bank * FITTED_BANK + within) >> 14) * 4 + ((within >> 7) & 3); + let old = *s.protection.get(&key).unwrap_or(&0); + if let Some(v) = write { + s.protection.insert(key, v); + } + return Some(old); + } + let within = ((at % (BANK_SPAN / 8)) & !15) * 8; + let field = s.config >> (bank / 2 * 8); + let code = (field >> 4) & 15; + if field & (1 << (bank & 1)) == 0 + || !(1..=5).contains(&code) + || within >= (32 << 20) << code + { + return self.refuse("directory", at); + } + let mask = if at & 8 != 0 || s.directory_config & 1 != 0 { + u64::MAX + } else { + u32::MAX as u64 + }; + let table; + let key; + if bank >= 2 || within >= FITTED_BANK { + if !s.compat.contains_key(&at) && s.compat.len() >= 74 { + return self.refuse("directory", at); + } + s.compat.entry(at).or_insert(0); + table = &mut s.compat; + key = at; + } else { + key = (bank * FITTED_BANK + within) / 128; + table = if at & 8 != 0 { + &mut s.counters + } else { + &mut s.directory + }; + } + let old = *table.get(&key).unwrap_or(&0) & mask; + if let Some(value) = write { + let value = if at & 8 == 0 && s.directory_config & 2 == 0 { + directory_encode(value, s.directory_config & 1 != 0) + } else { + value + }; + table.insert(key, value & mask); + if bank < 2 && within < FITTED_BANK && at & 8 == 0 { + if directory_syndrome(value, s.directory_config & 1 != 0) != 0 { + s.marked_directory.insert(key); + } else { + s.marked_directory.remove(&key); + } + } + } + Some(old) + } + fn checked_word( + &self, + s: &mut State, + bank: usize, + at: u64, + address: u64, + event: &mut Vec, + ) -> u64 { + let mut data = self.memory.raw_read(bank, at); + if s.ecc_config & 1 != 0 || s.ecc.is_empty() { + return data; + } + let key = (bank as u64 * FITTED_BANK + at) / 8; + if let Some(&check) = s.ecc.get(&key) { + let syndrome = check ^ ecc_compute(data); + if syndrome != 0 { + let bit = ECC_COLUMNS.iter().position(|&v| v == syndrome); + let valid = if bit.is_some() { 1 << 52 } else { 1 << 60 }; + if bit.is_none() + && s.memory_error & (1 << 60) != 0 + && (s.memory_error ^ address) & (((1u64 << 30) - 1) << 3) & !127 != 0 + { + s.memory_error |= 1 << 63; + } + if s.memory_error & (valid | (1 << 60)) != 0 { + s.memory_error |= valid << 1; + } else { + s.memory_error = (s.memory_error + & !((0xff << 40) | (((1u64 << 30) - 1) << 3))) + | ((syndrome as u64) << 40) + | (address & (((1u64 << 30) - 1) << 3)); + } + s.memory_error |= valid; + if let Some(bit) = bit { + if bit < 64 { + data ^= 1 << bit; + self.memory.raw_write(bank, at, data); + } + s.ecc.remove(&key); + event.push(MemoryError::Correctable); + } else { + event.push(MemoryError::Uncorrectable); + } + } + } + data + } + fn ram( + &self, + s: &mut State, + address: u64, + bytes: u64, + write: Option, + event: &mut Vec, + ) -> Option { + if let Some(page) = self.memory.startup_device(address, bytes) { + return Some(startup_access(page, address, bytes, write)); + } + let Some((bank, offset)) = BedrockMemory::decode(s.config, address, bytes) else { + return self.refuse("ram", address); + }; + if !s.marked_directory.is_empty() { + let line = (bank as u64 * FITTED_BANK + offset) / 128; + if s.marked_directory.contains(&line) { + let premium = s.directory_config & 1 != 0; + let word = *s.directory.get(&line).unwrap_or(&0); + if write.is_some() { + s.directory.insert(line, directory_encode(word, premium)); + s.marked_directory.remove(&line); + } else if s.directory_config & 2 == 0 { + let syndrome = directory_syndrome(word, premium); + if syndrome != 0 { + let bit = directory_columns(premium) + .iter() + .position(|&v| v == syndrome); + let valid = if bit.is_some() { 1 << 61 } else { 1 << 63 }; + if s.directory_error & ((1 << 63) | valid) != 0 { + s.directory_error |= if bit.is_some() { 1 << 57 } else { 1 << 59 }; + } else { + let carrier = 0x1c0000000 + + (address / BANK_SPAN) * (BANK_SPAN / 8) + + ((offset & !127) / 8); + s.directory_error = (s.directory_error + & !((0x7f << 40) | (((1u64 << 30) - 1) << 3))) + | ((syndrome as u64) << 40) + | (carrier & (((1u64 << 30) - 1) << 3)); + } + s.directory_error |= valid; + if let Some(bit) = bit { + s.directory.insert(line, word ^ (1 << bit)); + s.marked_directory.remove(&line); + event.push(MemoryError::Correctable); + } else { + event.push(MemoryError::Uncorrectable); + return None; + } + } + } + } + } + let at = offset & !7; + let shift = (8 - (offset & 7) - bytes) * 8; + let mask = u64::MAX >> ((8 - bytes) * 8); + if let Some(value) = write { + let old = self.memory.raw_read(bank, at); + self.memory.raw_write( + bank, + at, + (old & !(mask << shift)) | ((value & mask) << shift), + ); + if !s.ecc.is_empty() { + s.ecc.remove(&((bank as u64 * FITTED_BANK + at) / 8)); + } + Some(0) + } else { + Some((self.checked_word(s, bank, at, address, event) >> shift) & mask) + } + } + fn fetchop( + &self, + s: &mut State, + at: u64, + bytes: u64, + write: Option, + event: &mut Vec, + ) -> Option { + if bytes != 4 && bytes != 8 { + return self.refuse("fetchop", at); + } + let op = at & 0x38; + if op > if write.is_some() { 32 } else { 24 } { + return self.refuse("fetchop", at); + } + let address = (at & !63) | (at & 4); + let old = self.ram(s, address, bytes, None, event)?; + let next = match (write, op) { + (None, 0) => return Some(old), + (Some(v), 0) => v, + (_, 8) => old.wrapping_add(1), + (_, 16) => old.wrapping_sub(1), + (None, 24) => 0, + (Some(v), 24) => old & v, + (Some(v), 32) => old | v, + _ => return self.refuse("fetchop", at), + }; + self.ram(s, address, bytes, Some(next), event)?; + Some(old) + } + // Approximation: report whether the selected fitting covers the + // programmed bank, after 1000 guest ns. Only the zero write phase mutates + // memory; other patterns have no published transformation here. + fn selftest(&self, s: &mut State, command: u64) -> Option { + if command == 2 || command == 4 { + s.selftest_deadline = None; + s.selftest_status = 0; + s.selftest_error = 0; + return Some(0); + } + if command & 7 != 1 || command & !0x71 != 0 { + self.unsupported.fetch_add(1, Ordering::Relaxed); + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock MD ignored self-test command {command:#x}"); + return Some(0); + } + let selected = (command >> 4) & 7; + let configured = selected ^ ((s.config >> 32) & 3); + let field = s.config >> ((configured / 2) * 8); + let code = (field >> 4) & 15; + let size = if (1..=5).contains(&code) { + (32 << 20) << code + } else { + 0 + }; + let fits = selected < 2 + && field & (1 << (configured & 1)) != 0 + && size != 0 + && size <= FITTED_BANK; + s.selftest_result = fits; + s.selftest_status = 0; + s.selftest_error = 0; + s.selftest_deadline = Some(s.clock_ns.saturating_add(1000)); + if fits && s.storage.get(&0x20038).copied().unwrap_or(0) == 0x80 { + self.memory.zero(selected as usize, size); + let start = selected * FITTED_BANK; + s.ecc + .retain(|key, _| !(*key >= start / 8 && *key < (start + size) / 8)); + let lines: Vec = s + .marked_directory + .iter() + .copied() + .filter(|&line| line >= start / 128 && line < (start + size) / 128) + .collect(); + for line in lines { + let word = *s.directory.get(&line).unwrap_or(&0); + s.directory + .insert(line, directory_encode(word, s.directory_config & 1 != 0)); + s.marked_directory.remove(&line); + } + } + Some(0) + } + fn scrub( + &self, + s: &mut State, + at: u64, + bytes: u64, + write: Option, + event: &mut Vec, + ) -> Option { + if write.is_none() { + return self.ecc(s, at, bytes, None); + } + if bytes != 8 || at >= 1 << 30 { + return self.refuse("scrub", at); + } + let software = at / (BANK_SPAN / 8); + let within = at % (BANK_SPAN / 8); + let address = software * BANK_SPAN + within * 8; + let Some((bank, offset)) = BedrockMemory::decode(s.config, address, 8) else { + return self.refuse("scrub", at); + }; + for i in 0..8 { + if BedrockMemory::decode(s.config, address + i * 8, 8).is_some() { + self.checked_word(s, bank, offset + i * 8, address + i * 8, event); + } + } + Some(0) + } + fn ecc(&self, s: &mut State, at: u64, bytes: u64, write: Option) -> Option { + if at >= 1 << 30 || (bytes != 1 && bytes != 2) { + return self.refuse("ecc", at); + } + let software = at / (BANK_SPAN / 8); + let within = (at % (BANK_SPAN / 8)) ^ if bytes == 1 { 7 } else { 6 }; + let address = software * BANK_SPAN + within * 8; + let Some((bank, offset)) = BedrockMemory::decode(s.config, address, bytes * 8) else { + if bytes == 2 && [0x04000006, 0x08000006].contains(&at) { + let i = usize::from(at == 0x08000006); + let old = s.admitted[i]; + if let Some(value) = write { + s.admitted[i] = value as u16; + } + return Some(old as u64); + } + return self.refuse("ecc", at); + }; + let mut old = 0; + for i in 0..bytes { + let raw = offset + i * 8; + let key = (bank as u64 * FITTED_BANK + raw) / 8; + let computed = ecc_compute(self.memory.raw_read(bank, raw)); + old |= (s.ecc.get(&key).copied().unwrap_or(computed) as u64) << (i * 8); + if let Some(value) = write { + let code = (value >> (i * 8)) as u8; + if code == computed { + s.ecc.remove(&key); + } else { + s.ecc.insert(key, code); + } + } + } + Some(old) + } +} +impl Resettable for BedrockMd { + fn power_on(&self) { + // Reset is called on the CPU thread or with it stopped. + let _remapping = self.remapping.lock(); + *self.state.lock() = State::default(); + let was_live = self.memory.startup_live(); + self.memory.clear_startup(); + self.memory.remap(0, BTreeSet::new(), was_live); + } +} +/// Resolve each DMA access through the atomic Physical table, as HEART +/// does. Do not ask Physical for mem_ptr or gen_ptr: those read the inline +/// CPU bitmap. Default block methods split accesses and respect slow pages +/// even if a caller supplies a block crossing a 4 KiB page boundary. +struct DmaView(Arc); +impl DmaView { + fn at(&self, address: u64, bytes: u64) -> Option<&dyn BusDevice> { + if let Some(page) = self.0.memory.startup_device(address, bytes) { + return Some(page); + } + BedrockMemory::decode(self.0.memory.config(), address, bytes)?; + let phys = self.0.physical(); + let entry = phys.entry_at(address); + if crate::ppmem::devmap::entry_eq(entry, phys.default_entry()) { + return None; + } + // SAFETY: the owning MD retains Physical, which keeps every entry + // alive across remaps, including the weak MD slow endpoint. + Some(unsafe { &*entry }) + } +} +macro_rules! dma_rw { + ($read:ident, $write:ident, $result:ident, $ty:ty) => { + fn $read(&self, address: u64) -> $result { + self.at(address, std::mem::size_of::<$ty>() as u64) + .map_or($result::err(), |device| device.$read(address)) + } + fn $write(&self, address: u64, value: $ty) -> u32 { + self.at(address, std::mem::size_of::<$ty>() as u64) + .map_or(BUS_ERR, |device| { + let result = device.$write(address, value); + if result == BUS_OK { self.0.memory.written(address); } + result + }) + } + }; +} +impl BusDevice for DmaView { + dma_rw!(read8, write8, BusRead8, u8); + dma_rw!(read16, write16, BusRead16, u16); + dma_rw!(read32, write32, BusRead32, u32); + dma_rw!(read64, write64, BusRead64, u64); + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + self.0.memory.gen_ptr(address) + } +} + +/// A read or write of `bytes` at `address` in the startup page. The page is +/// plain memory: no ECC, no directory, no fetch-and-op. +fn startup_access(page: &dyn BusDevice, address: u64, bytes: u64, write: Option) -> u64 { + match (bytes, write) { + (1, None) => page.read8(address).data as u64, + (2, None) => page.read16(address).data as u64, + (4, None) => page.read32(address).data as u64, + (_, None) => page.read64(address).data, + (1, Some(v)) => { page.write8(address, v as u8); 0 } + (2, Some(v)) => { page.write16(address, v as u16); 0 } + (4, Some(v)) => { page.write32(address, v as u32); 0 } + (_, Some(v)) => { page.write64(address, v); 0 } + } +} + +impl BusDevice for MdBus { + /// Bedrock's ECC view is the one device that sees the whole source register + /// of an uncached SH (`BusDevice::write_narrow_from`); every other view + /// takes the architectural low bits. + fn write_narrow_from(&self, addr: u64, size: usize, gpr: u64) -> u32 { + if size == 2 && matches!(self.view, View::Ecc) { + // PROM 6.210's memory test stores each check word with `sh` from a + // register whose low 16 bits are zero: the word sits elsewhere in + // the register, by the address of the memory it checks. Observed + // (memory address mod 0x40, carrier offset mod 8, register bits): + // 0x00 6 15:0 + // 0x10 4 63:48 + // 0x20 2 47:32 + // 0x30 0 31:16 + // The shift below gives exactly these four. Which lanes the R14000 + // drives for a narrow store, and how Bedrock wires them to the + // check bits, is unknown; this matches the PROM, not a hardware + // description. + let memory_address = (addr ^ 6).wrapping_mul(8); + return self.write16(addr, (gpr >> (memory_address.wrapping_neg() & 63)) as u16); + } + if size == 1 { self.write8(addr, gpr as u8) } else { self.write16(addr, gpr as u16) } + } + + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const std::sync::atomic::AtomicU64 { + let Some(md) = self.md.upgrade() else { + return std::ptr::null(); + }; + if !matches!(self.view, View::Memory | View::Uncached) { + return std::ptr::null(); + } + md.memory.gen_ptr(address) + } + + fn read8(&self, a: u64) -> BusRead8 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 1, None)) + .map_or(BusRead8::err(), |v| BusRead8::ok(v as u8)) + } + fn read16(&self, a: u64) -> BusRead16 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 2, None)) + .map_or(BusRead16::err(), |v| BusRead16::ok(v as u16)) + } + fn read32(&self, a: u64) -> BusRead32 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 4, None)) + .map_or(BusRead32::err(), |v| BusRead32::ok(v as u32)) + } + fn read64(&self, a: u64) -> BusRead64 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 8, None)) + .map_or(BusRead64::err(), BusRead64::ok) + } + fn write8(&self, a: u64, v: u8) -> u32 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 1, Some(v as u64))) + .map_or(BUS_ERR, |_| BUS_OK) + } + fn write16(&self, a: u64, v: u16) -> u32 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 2, Some(v as u64))) + .map_or(BUS_ERR, |_| BUS_OK) + } + fn write32(&self, a: u64, v: u32) -> u32 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 4, Some(v as u64))) + .map_or(BUS_ERR, |_| BUS_OK) + } + fn write64(&self, a: u64, v: u64) -> u32 { + self.md + .upgrade() + .and_then(|m| m.access(self.view, a, 8, Some(v))) + .map_or(BUS_ERR, |_| BUS_OK) + } +} +fn storage_mask(at: u64) -> Option { + Some(match at { + reg::ARBITRATION => 0xff, + reg::MIGRATION_CONFIG => 0x3ff | (0xff << 16) | (1 << 32), + reg::PERF_SELECT0 | reg::PERF_SELECT1 => { + 3 | (15 << 4) | (127 << 8) | (15 << 16) | (0xffffffffff << 20) | (1 << 63) + } + reg::PERF_COUNTER0 | reg::PERF_COUNTER1 => (1 << 41) - 1, + reg::JUNK_BUS_TIMING => 0xffffffffffff, + reg::SELFTEST_DATA => 0xff, + reg::ISSUE_DEBUG => 3, + reg::MIGRATION_DIFFERENCE => 0x7fff | (7 << 32) | (7 << 35) | (1 << 63), + reg::MIGRATION_VALUE => 0x7fff | (7 << 32) | (7 << 35) | (3 << 62), + reg::DIRECTORY_DEBUG => 3, + reg::MEMORY_DEBUG => 1, + _ => return None, + }) +} +fn idle(at: u64) -> bool { + matches!( + at, + 0x20 | 0x28 + | 0x40 + | 0x48 + | 0x58 + | 0x20010 + | 0x20018 + | 0x20020 + | 0x20028 + | 0x20030 + | 0x20040 + | 0x20048 + | 0x40020 + | 0x40028 + | 0x40030 + | 0x40038 + | 0x40058 + | 0x60020 + | 0x60028 + | 0x60030 + | 0x60038 + ) +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sn1-bedrock-md-test.c in + // jamesbraid/qemu-sgi-origami: memory-banks, reset, queue-sizes, + // writable-masks, selftest-phases, fetchop-operations, ecc-carrier and + // directory-storage. + use super::*; + fn configured() -> Arc { + let md = BedrockMd::new(); + assert_eq!(md.md_bus().write64(8, 0x33), BUS_OK); + md + } + + /// What PROM 6.210 stores as primary data cache tags (TagLo), measured: + /// test patterns, then an invalidate, then a valid tag per page of the + /// window, then the invalidate sweep it leaves. + const WINDOW_TAGS: [u32; 8] = [0x100c0, 0x101c1, 0x102c0, 0x103c1, 0x104c0, 0x105c1, 0x106c0, 0x107c1]; + + /// A valid tag for a page of the window starts a private page that every + /// path reaches, whether or not DRAM is configured; an invalid tag ends it + /// and leaves memory as it was. + #[test] + fn a_valid_tag_for_the_window_starts_a_page_every_path_answers() { + let md = BedrockMd::new(); + let phys = md.physical(); + let (dma, uncached) = (md.dma_view(), md.uncached_view()); + let at = 0x10_0008 + 0x7000; + assert_eq!(phys.read64(at).status, BUS_ERR, "no memory behind the window yet"); + md.startup_tag_store(WINDOW_TAGS[0]); + assert_eq!(phys.write64(at, 0x1122_3344_5566_7788), BUS_OK); + assert_eq!(phys.read64(at).data, 0x1122_3344_5566_7788); + assert_eq!(dma.read64(at).data, 0x1122_3344_5566_7788, "the cached path and DMA"); + assert_eq!(uncached.read32(at + 4).data, 0x5566_7788, "the uncached path"); + assert_eq!(uncached.write8(at, 0xaa), BUS_OK); + assert_eq!(phys.read64(at).data, 0xaa22_3344_5566_7788); + assert_eq!(phys.read64(0x10_8000).status, BUS_ERR, "only the window's 32 KiB"); + // The page wraps in itself, and the bank behind it is untouched. + assert_eq!(md.md_bus().write64(reg::CONFIG, 0x33), BUS_OK); + assert_eq!(phys.read64(at).data, 0xaa22_3344_5566_7788, "configuring DRAM keeps the page"); + assert_eq!(phys.banks()[0].read64(at).data, 0, "while it stands, memory is not the page"); + md.startup_tag_store(0); + assert_eq!(phys.read64(at).data, 0, "the window is memory again"); + assert_eq!(phys.banks()[0].read64(at).data, 0); + } + + /// Test patterns, a tag outside the window, and a kernel's invalid tags + /// leave the decode alone; only the window's valid tags start the page. + #[test] + fn other_tags_leave_the_window_to_memory() { + let md = BedrockMd::new(); + for tag in [0xffff_ffcf, 0xaaaa_aa8a, 0x5555_5545, 0, 0x108c0, 0xff00c0, 0x1000c0 >> 4] { + md.startup_tag_store(tag); + assert!(!md.memory.startup_live(), "{tag:#x}"); + } + assert_eq!(md.physical().read64(0x10_0000).status, BUS_ERR); + // The tag's node bits above the node's 8 GiB do not move the window. + md.startup_tag_store(0x2001_00c0); + assert!(md.memory.startup_live()); + } + + /// The whole PROM pattern: the page starts at the first install, stays + /// through the rest and across a configuration write, and ends at the + /// first invalid tag of the final sweep. + #[test] + fn the_proms_tag_sequence_starts_and_ends_the_page_once() { + let md = BedrockMd::new(); + let phys = md.physical(); + for tag in [0xffff_ffcf, 0xaaaa_aa8a, 0x5555_5545, 0] { + md.startup_tag_store(tag); + } + assert!(!md.memory.startup_live()); + for tag in WINDOW_TAGS { + md.startup_tag_store(tag); + assert!(md.memory.startup_live()); + } + assert_eq!(phys.write64(0x10_0000, 7), BUS_OK); + md.md_bus().write64(reg::CONFIG, 0x33); + md.soft_reset(); + assert_eq!(phys.read64(0x10_0000).data, 7, "a controller soft reset is not a power-on"); + md.startup_tag_store(0); + assert!(!md.memory.startup_live()); + md.startup_tag_store(0); + assert!(!md.memory.startup_live()); + // A reboot through the PROM tags again, and the cache still holds + // what the page held. + md.startup_tag_store(WINDOW_TAGS[7]); + assert!(md.memory.startup_live()); + assert_eq!(phys.read64(0x10_0000).data, 7); + } + + /// The final sweep stores 2050 invalid tags; the page ends at the first, + /// and the PROM's later stores to the window reach memory, not the page. + #[test] + fn the_page_ends_at_the_first_invalid_tag_of_the_final_sweep() { + let md = configured(); + let phys = md.physical(); + for tag in WINDOW_TAGS { + md.startup_tag_store(tag); + } + assert_eq!(phys.write64(0x10_0000, 5), BUS_OK); + md.startup_tag_store(0); + assert!(!md.memory.startup_live(), "the first invalid store ends it"); + assert_eq!(phys.read64(0x10_0000).data, 0); + assert_eq!(phys.write64(0x10_0000, 6), BUS_OK); + for _ in 1..2050 { + md.startup_tag_store(0); + } + assert!(!md.memory.startup_live()); + assert_eq!(phys.read64(0x10_0000).data, 6, "memory, untouched by the rest of the sweep"); + } + + /// While the page stands its 64 MiB region is out of the direct window, + /// so cached loads and the JIT reach it through the decode; the rest of + /// the region comes back with the page's end. + #[test] + fn the_page_keeps_its_region_out_of_the_direct_window() { + use crate::ppmem::MappedMemory; + let md = configured(); + let space = md.physical().ppmem_space().unwrap(); + assert_ne!(space.mapped_bitmap() & 1, 0, "configured memory is in the window"); + md.startup_tag_store(WINDOW_TAGS[0]); + assert_eq!(space.mapped_bitmap() & 1, 0, "while the page stands"); + assert_ne!(space.mapped_bitmap() & 2, 0, "the next region is not affected"); + md.startup_tag_store(0); + assert_ne!(space.mapped_bitmap() & 1, 0, "and it returns"); + } + + /// Power-on drops the page, zeroed, and its decode. + #[test] + fn power_on_clears_the_page() { + use crate::traits::Resettable; + let md = configured(); + md.startup_tag_store(WINDOW_TAGS[0]); + assert_eq!(md.physical().write64(0x10_0000, 9), BUS_OK); + md.power_on(); + assert!(!md.memory.startup_live()); + assert_eq!(md.physical().read64(0x10_0000).status, BUS_ERR, "no memory until configured"); + md.startup_tag_store(WINDOW_TAGS[0]); + assert_eq!(md.physical().read64(0x10_0000).data, 0, "the page starts zero"); + } + + /// Ordinary CPU and DMA RAM accesses remain available while MD state is locked. + #[test] + fn ordinary_cpu_and_dma_memory_do_not_take_the_md_lock() { + let md = configured(); + let dma = md.dma_view(); + let _state = md.state.lock(); + assert_eq!(md.physical().write64(0x1000, 0x1122334455667788), BUS_OK); + assert_eq!(md.physical().read64(0x1000).data, 0x1122334455667788); + assert_eq!(dma.read64(0x1000).data, 0x1122334455667788); + assert_eq!(dma.write32(0x1004, 0xabcdef01), BUS_OK); + assert_eq!(md.physical().read64(0x1000).data, 0x11223344abcdef01); + assert_eq!(dma.read64(FITTED_BANK).status, BUS_ERR); + } + + /// An ECC injection removes its direct mapping; DMA repair keeps the page slow. + #[test] + fn injected_checks_disable_the_window_and_dma_repair_keeps_the_slow_page() { + use crate::ppmem::MappedMemory; + let md = configured(); + let phys = md.physical(); + let space = phys.ppmem_space().expect("a window"); + assert_eq!(space.mapped_bitmap() & 3, 3); + assert!(phys.mem_ptr(0).is_some()); + assert_eq!(md.ecc_bus().write8(7, 0x13), BUS_OK); + assert_eq!(space.mapped_bitmap() & 3, 2, "the full 64 MiB region loses its direct bit"); + assert!(phys.mem_ptr(0).is_none()); + let slow = phys.entry_at(0); + assert!(!crate::ppmem::devmap::entry_eq(slow, phys.entry_at(0x1000))); + let dma = md.dma_view(); + let repaired = std::thread::spawn(move || dma.read64(0)).join().unwrap(); + assert_eq!(repaired.status, BUS_OK); + assert_eq!(repaired.data, 1); + assert!(md.state.lock().ecc.is_empty()); + assert_eq!(space.mapped_bitmap() & 3, 2, "a DMA thread never republishes the CPU bitmap"); + assert!(crate::ppmem::devmap::entry_eq(slow, phys.entry_at(0))); + // Adjacent pages still use the ordinary bank entry, even though + // the region no longer has a direct window mapping. + { + let _state = md.state.lock(); + assert_eq!(phys.write64(0x1000, 9), BUS_OK); + assert_eq!(phys.read64(0x1000).data, 9); + } + md.soft_reset(); + assert_eq!(space.mapped_bitmap() & 3, 3); + assert!(phys.mem_ptr(0).is_some()); + assert_eq!(phys.read64(0).data, 1); + } + + /// Directory-marked pages follow their physical bank after a configuration swap. + #[test] + fn marked_directory_pages_follow_their_bank_when_swizzled() { + use crate::ppmem::MappedMemory; + let md = configured(); + let phys = md.physical(); + let space = phys.ppmem_space().expect("a window"); + md.md_bus().write64(reg::DIRECTORY_CONFIG, 2); + md.directory_bus().write64(0, 0x001e0001); + assert_eq!(space.mapped_bitmap() & 1, 0); + assert!(phys.mem_ptr(0).is_none()); + md.md_bus().write64(reg::CONFIG, 0x33 | (1 << 32)); + assert_ne!(space.mapped_bitmap() & 1, 0, "the other bank now answers at zero"); + assert_eq!(space.mapped_bitmap() & (1 << 16), 0); + assert!(phys.mem_ptr(BANK_SPAN).is_none()); + md.md_bus().write64(reg::DIRECTORY_CONFIG, 0); + assert_eq!(phys.read64(BANK_SPAN).status, BUS_OK); + assert_ne!(md.md_bus().read64(reg::DIRECTORY_ERROR).data & (1 << 61), 0); + md.rebuild_memory(); + assert!(phys.mem_ptr(BANK_SPAN).is_some()); + md.power_on(); + assert_eq!(space.mapped_bitmap(), 0); + assert_eq!(phys.read64(BANK_SPAN).status, BUS_ERR); + } + + /// Guest code must not keep a compiled page after its backing bank changes. + /// page_for deliberately retains its generation pointer. A remap must + /// invalidate that page and subsequent writes to its new bank must keep + /// invalidating the same pointer, including after swapping back. + #[cfg(feature = "jitv2")] + #[test] + fn a_claimed_jit_page_tracks_physical_ownership_across_bank_swaps() { + use crate::cpu::jitv2::jitv2::Jitv2; + let md = configured(); + let dma = md.dma_view(); + let mut jit = Jitv2::new(4); + let slot = jit.page_for(0, 0, dma.as_ref(), false).unwrap(); + let page = jit.page_ptr(slot); + let generation = || unsafe { (*page).current_gen() }; + let pointer = dma.gen_ptr(0); + let mut entries = [0u64; crate::cpu::jitv2::jitv2::BITMAP_WORDS]; + entries[0] = 1; + // Exercise the JIT publication/invalidation protocol without + // executing a synthetic function body. + let code = std::ptr::NonNull::<()>::dangling().as_ptr() as *const (); + assert!(unsafe { (*page).publish(&entries, code, generation(), 1, 0, false) }); + assert!(unsafe { (*page).is_runnable(0) }); + let first = generation(); + md.md_bus().write64(reg::CONFIG, 0x33 | (1 << 32)); + assert_eq!(jit.page_for(0, 0, dma.as_ref(), false), Some(slot)); + assert_eq!(dma.gen_ptr(0), pointer); + assert!(generation() > first); + assert!(!unsafe { (*page).is_runnable(0) }); + let swapped = generation(); + assert!(unsafe { (*page).publish(&entries, code, swapped, 1, 0, false) }); + assert!(unsafe { (*page).is_runnable(0) }); + assert_eq!(dma.write64(0, 0x11223344), BUS_OK); + assert!(generation() > swapped); + assert!(!unsafe { (*page).is_runnable(0) }, "code published after a swap sees new-bank writes"); + assert_eq!(md.physical().banks()[1].read64(0).data, 0x11223344); + let written = generation(); + md.md_bus().write64(reg::CONFIG, 0x33); + assert!(generation() > written, "swapping back cannot lower the logical generation"); + let restored = generation(); + md.uncached_view().write64(0, 0x55667788); + assert!(generation() > restored); + let before_fetchop = generation(); + md.fetchop_bus().write64(0, 0); + assert!(generation() > before_fetchop); + md.ecc_bus().write8(7, 0x13); + let before_repair = generation(); + assert_eq!(dma.read64(0).data, 1); + assert!(generation() > before_repair, "ECC repair invalidates logical code pages"); + md.md_bus().write64(reg::CONFIG, 0x13); + md.md_bus().write64(0x20038, 0x80); + let before_zero = generation(); + md.md_bus().write64(reg::SELFTEST_COMMAND, 1); + assert!(generation() > before_zero, "zero self-test invalidates logical code pages"); + let before_disable = generation(); + md.power_on(); + assert!(generation() > before_disable); + assert_eq!(dma.gen_ptr(0), pointer, "a disabled bank keeps its logical counter alive"); + } + + /// Guest density settings remap fitted RAM, and reset restores the default map. + #[test] + fn bank_configuration_remaps_memory_and_reset_restores_defaults() { + let md = configured(); + let bus = md.physical(); + assert_eq!(bus.write64(0, 0x1122334455667788), BUS_OK); + assert_eq!(bus.write64(BANK_SPAN, 0xabcdef), BUS_OK); + assert_eq!(bus.read8(0).data, 0x11); + assert_eq!(bus.read32(4).data, 0x55667788); + assert_eq!(bus.read64(FITTED_BANK).status, BUS_ERR); + assert_eq!(bus.read64(2 * BANK_SPAN).status, BUS_ERR); + assert_eq!(md.md_bus().write64(8, 0x33 | 1 << 32), BUS_OK); + assert_eq!(bus.read64(0).data, 0xabcdef); + assert_eq!(bus.read64(BANK_SPAN).data, 0x1122334455667788); + assert!(bus.mem_ptr(0).is_some()); + md.soft_reset(); + assert_eq!(bus.read64(0).data, 0xabcdef); + md.power_on(); + assert_eq!(bus.read64(0).status, BUS_ERR); + bus.map_unmapped(0x1fc00000, 0x1fc01000); + for (code, size) in [(1, 64 << 20), (2, 128 << 20), (3, FITTED_BANK), + (4, FITTED_BANK), (5, FITTED_BANK)] { + md.md_bus().write64(reg::CONFIG, 1 | (code << 4)); + assert_eq!(bus.read64(size - 8).status, BUS_OK); + assert_eq!(bus.read64(size).status, BUS_ERR); + assert_eq!(bus.read64(0x1fc00000).status, BUS_OK, "a non-RAM mapping survives"); + } + md.md_bus().write64(reg::CONFIG, 0x11); + assert_eq!(bus.read64(64 << 20).status, BUS_ERR, "the old prefix is removed on shrink"); + md.md_bus().write64(reg::CONFIG, 0x61); + assert_eq!(bus.read64(0).status, BUS_ERR, "unsupported density does not map RAM"); + #[cfg(feature = "jitv2")] + { + let md = configured(); + let ram = md.physical(); + let first = ram.gen_ptr(0); + let second = ram.gen_ptr(BANK_SPAN); + assert!(!first.is_null() && !second.is_null()); + assert_ne!(first, second); + let before = unsafe { (*first).load(Ordering::Relaxed) }; + ram.write64(0, 0x55); + assert!(unsafe { (*first).load(Ordering::Relaxed) } > before); + md.md_bus().write64(8, 0x33 | (1 << 32)); + // The window address stays fixed while its backing bank changes. + assert_eq!(ram.gen_ptr(0), first); + assert_eq!(ram.gen_ptr(BANK_SPAN), second); + let bank0 = ram.banks()[0].gen_ptr(0); + let bank1 = ram.banks()[1].gen_ptr(0); + let before0 = unsafe { (*bank0).load(Ordering::Relaxed) }; + let before1 = unsafe { (*bank1).load(Ordering::Relaxed) }; + assert_eq!(ram.write64(0, 0xaa), BUS_OK); + assert_eq!(unsafe { (*bank0).load(Ordering::Relaxed) }, before0); + assert_eq!(unsafe { (*bank1).load(Ordering::Relaxed) }, before1 + 1); + assert_eq!(unsafe { (*first).load(Ordering::Relaxed) }, before1 + 1); + let view = md.uncached_view(); + let logical = view.gen_ptr(0); + assert_eq!(logical, md.memory.gen_ptr(0)); + let logical_before = unsafe { (*logical).load(Ordering::Relaxed) }; + drop(md); + assert_eq!(unsafe { (*logical).load(Ordering::Relaxed) }, logical_before, + "an external endpoint keeps its logical generation pointers alive"); + // The endpoint keeps the backing memory alive after MD disappears. + assert!(unsafe { (*bank1).load(Ordering::Relaxed) } > 0); + assert_eq!(view.read64(0).status, BUS_ERR); + } + + } + /// The guest reads ECC configuration and memory or directory errors through status registers. + #[test] + fn guest_registers_expose_ecc_configuration_and_error_latches() { + { + let md = configured(); + let r = md.md_bus(); + // These carrier offsets are idle; they do not select storage. + for at in [0x20, 0x20010, 0x40020, 0x60020] { + let before = md.unsupported(); + assert_eq!(r.write64(at, u64::MAX), BUS_OK, "{at:#x}"); + assert_eq!(r.read64(at).data, 0, "{at:#x}"); + assert_eq!(md.unsupported(), before + 1); + } + for (at, value) in [(0x60, 0x001818180c0c0c00), (0x40050, 0x04040400)] { + let before = md.unsupported(); + assert_eq!(r.write64(at, 0), BUS_OK); + assert_eq!(r.read64(at).data, value); + assert_eq!(md.unsupported(), before + 1); + } + // Error-clear writes are ignored too: only a read clears the record. + md.ecc_bus().write8(7, 3); + md.uncached_view().read64(0); + r.write64(0x40000, 2); + md.directory_bus().write64(0, 3 << 16); + r.write64(0x40000, 0); + md.physical().read64(0); + let memory_error = r.read64(0x60010).data; + let directory_error = r.read64(0x40010).data; + assert_ne!(memory_error & (1 << 60), 0); + assert_ne!(directory_error & (1 << 63), 0); + for at in [0x40010, 0x40018, 0x60010, 0x60018] { + let before = md.unsupported(); + assert_eq!(r.write64(at, u64::MAX), BUS_OK); + assert_eq!(r.read64(0x60010).data, memory_error); + assert_eq!(r.read64(0x40010).data, directory_error); + assert_eq!(md.unsupported(), before + 1); + } + // An unsupported self-test command leaves an in-flight command alone. + r.write64(0x20030, 1); + let before = md.unsupported(); + assert_eq!(r.write64(0x20030, 3), BUS_OK); + assert_eq!(md.unsupported(), before + 1); + md.advance_ns(1000); + assert_eq!(r.read64(0x20048).data, 3); + assert_eq!(r.write64(0x70000, 1), BUS_ERR); + assert_eq!(r.read64(0x70000).status, BUS_ERR); + assert_eq!(r.write32(0x60, 0), BUS_ERR); + assert_eq!(r.write64(0x61, 0), BUS_ERR); + } + { + let md = configured(); + let regs = md.md_bus(); + assert_eq!(regs.read64(0x60000).data, 0); + assert_eq!(regs.write64(0x60000, u64::MAX), BUS_OK); + assert_eq!(regs.read64(0x60000).data, 1); + assert_eq!(regs.write64(0x60000, !1), BUS_OK); + assert_eq!(regs.read64(0x60000).data, 0); + assert_eq!(regs.write8(0x60000, 1), BUS_ERR); + assert_eq!(regs.write64(0x60001, 1), BUS_ERR); + regs.write64(0x60000, 1); + md.soft_reset(); + assert_eq!(regs.read64(0x60000).data, 0); + assert_eq!(regs.read64(8).data, 0x33); + regs.write64(0x60000, 1); + md.power_on(); + assert_eq!(regs.read64(0x60000).data, 0); + assert_eq!(regs.read64(8).data, 0); + } + { + for (code, kind, status, valid) in [ + (1, MemoryError::Correctable, BUS_OK, 1 << 52), + (3, MemoryError::Uncorrectable, BUS_ERR, 1 << 60), + ] { + let md = configured(); + let regs = md.md_bus(); + let ecc = md.ecc_bus(); + let ram = md.uncached_view(); + let events = Arc::new(Mutex::new(Vec::new())); + let seen = events.clone(); + md.on_error(Arc::new(move |event| seen.lock().push(event))); + assert_eq!(ram.read64(0).status, BUS_OK); + ecc.write8(7, code); + regs.write64(0x60000, u64::MAX); + assert_eq!(ram.read64(0).status, BUS_OK); + assert_eq!(ecc.read8(7).data, code); + assert_eq!(regs.read64(0x60010).data, 0); + assert!(events.lock().is_empty()); + regs.write64(0x60000, !1); + assert_eq!(regs.read64(0x60000).data, 0); + assert_eq!(ram.read64(0).status, status); + assert_ne!(regs.read64(0x60010).data & valid, 0); + assert_eq!(*events.lock(), vec![kind]); + if kind == MemoryError::Correctable { + assert_eq!(ecc.read8(7).data, 0); + assert_eq!(ram.read64(0).status, BUS_OK); + assert_eq!(*events.lock(), vec![kind]); + } + } + } + { + let md = configured(); + let regs = md.md_bus(); + let ecc = md.ecc_bus(); + let ram = md.uncached_view(); + ecc.write8(7, 3); + regs.write64(0x60000, 1); + assert_eq!(ram.write8(7, 0x5a), BUS_OK); + assert_eq!(ecc.read8(7).data, 0x82); + regs.write64(0x60000, 0); + assert_eq!(ram.read64(0).status, BUS_OK); + assert_eq!(ram.read64(0).data, 0x5a); + assert_eq!(regs.read64(0x60010).data, 0); + } + } + /// Self-test completion is visible only after its configured clock interval. + #[test] + fn selftest_waits_for_fitted_clock_cycles_and_zero_fills_ram() { + { + let now = Arc::new(AtomicU64::new(100)); + let time = now.clone(); + let md = BedrockMd::with_clock(Arc::new(move || time.load(Ordering::Relaxed))); + let regs = md.md_bus(); + regs.write64(8, 0x33); + md.start_clock(); + regs.write64(0x20030, 1); + now.store(1099, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + now.store(1100, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 3); + now.store(5000, Ordering::Relaxed); + regs.write64(0x20030, 1); + now.store(5500, Ordering::Relaxed); + md.stop_clock(); + now.store(15000, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + md.start_clock(); + now.store(15499, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + now.store(15500, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 3); + regs.write64(0x20030, 1); + now.store(15400, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + now.store(16499, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + now.store(16500, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 3); + regs.write64(0x20030, 1); + regs.write64(0x20030, 2); + now.store(20000, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + md.power_on(); + regs.write64(8, 0x33); + md.start_clock(); + regs.write64(0x20030, 1); + now.store(20999, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 0); + now.store(21000, Ordering::Relaxed); + assert_eq!(regs.read64(0x20048).data, 3); + } + { + let md = configured(); + let r = md.md_bus(); + r.write64(0x20030, 0x21); + md.advance_ns(1000); + assert_eq!(r.read64(0x20048).data, 2); + assert_eq!(r.read64(0x20040).data, 1 << 48); + r.write64(0x20030, 1); + r.write64(0x20030, 2); + md.advance_ns(1000); + assert_eq!(r.read64(0x20048).data, 0); + assert_eq!(r.write64(0x20030, 3), BUS_OK); + } + { + let md = configured(); + let r = md.md_bus(); + r.write64(8, 0x13); + md.physical().write64(0, 0x1234); + md.ecc_bus().write8(7, 1); + r.write64(0x20038, 0x80); + r.write64(0x20030, 1); + md.advance_ns(1000); + assert_eq!(r.read64(0x20048).data, 3); + assert_eq!(md.physical().read64(0).data, 0); + assert_eq!(md.ecc_bus().read8(7).data, 0); + assert_eq!(r.read64(0x60010).data, 0); + } + } + /// Fetch-and-op registers preserve the big-endian lanes used by the processor. + #[test] + fn fetchop_preserves_big_endian_word_lanes() { + let md = configured(); + let ram = md.physical(); + let f = md.fetchop_bus(); + ram.write64(0, 0xffffffff00000009); + assert_eq!(f.read32(8).data, u32::MAX); + assert_eq!(ram.read64(0).data, 9); + assert_eq!(f.read32(12).data, 9); + assert_eq!(ram.read64(0).data, 10); + assert_eq!(f.write32(28, 3), BUS_OK); + assert_eq!(ram.read64(0).data, 2); + assert_eq!(f.write64(32, 8), BUS_OK); + assert_eq!(ram.read64(0).data, 10); + assert_eq!(f.read64(24).data, 10); + assert_eq!(ram.read64(0).data, 0); + assert_eq!(f.read64(32).status, BUS_ERR); + assert_eq!(f.write64(40, 1), BUS_ERR); + assert_eq!(f.read16(0).status, BUS_ERR); + } + /// The guest reads ECC bytes and scrubbing repairs correctable memory. + #[test] + fn ecc_carriers_expose_check_bytes_and_scrub_corrections() { + { + let md = configured(); + let ecc = md.ecc_bus(); + for carrier in 0..8 { + let source = 0x11223344556677a5u64; + assert_eq!(ecc.write8(carrier, source as u8), BUS_OK); + assert_eq!(ecc.read8(carrier).data, source as u8, "carrier {carrier}"); + } + assert_eq!(ecc.write16(4, 0x1234), BUS_OK); + assert_eq!(ecc.read16(4).data, 0x1234); + assert_eq!(ecc.write16(6, 0x2313), BUS_OK); + assert_eq!(ecc.read16(6).data, 0x2313); + assert_eq!(ecc.write16(0x04000006, 0xface), BUS_OK); + assert_eq!(ecc.read16(0x04000006).data, 0xface); + + } + { + let md = configured(); + let events = Arc::new(Mutex::new(Vec::new())); + let seen = events.clone(); + let weak = Arc::downgrade(&md); + md.on_error(Arc::new(move |kind| { + assert_eq!(weak.upgrade().unwrap().current_cell(), u64::MAX); + seen.lock().push(kind); + })); + assert_eq!(md.ecc_bus().write8(7, 3), BUS_OK); + assert_eq!(md.ecc_bus().write8(6, 0x13), BUS_OK); + assert_eq!(md.ecc_bus().write8(5, 0x23), BUS_OK); + assert_eq!(md.ecc_scrub_bus().write64(0, 0), BUS_OK); + assert_eq!( + *events.lock(), + vec![ + MemoryError::Uncorrectable, + MemoryError::Correctable, + MemoryError::Correctable, + ] + ); + let record = md.md_bus().read64(0x60010).data; + assert_ne!(record & (1 << 60), 0); + assert_ne!(record & (1 << 52), 0); + assert_ne!(record & (1 << 53), 0); + assert_eq!((record >> 40) & 0xff, 3); + assert_eq!(record & (((1u64 << 30) - 1) << 3), 0); + assert_eq!(md.ecc_bus().read8(6).data, 0x13); + assert_eq!(md.ecc_bus().read8(5).data, 0x23); + } + } + /// The ECC view takes an uncached SH's check word from wherever the PROM + /// put it in the source register; other views, and bytes, take the low bits. + #[test] + fn ecc_view_reads_the_check_word_from_the_whole_source_register() { + let md = configured(); + let ecc = md.ecc_bus(); + // (carrier offset, register bits holding the check word) + for (carrier, shift) in [(6u64, 0u32), (4, 48), (2, 32), (0, 16)] { + let source = 0x1111_2222_3333_4444u64 ^ (0xa55a_u64 << shift); + let word = (source >> shift) as u16; + assert_eq!(ecc.write_narrow_from(carrier, 2, source), BUS_OK); + assert_eq!(ecc.read16(carrier).data, word, "carrier {carrier}"); + } + assert_eq!(ecc.write_narrow_from(5, 1, 0xfeed_0000_0000_0023), BUS_OK); + assert_eq!(ecc.read8(5).data, 0x23, "a byte keeps the architectural low bits"); + // The memory view is not the ECC view: its halfword is the low bits. + let view = md.uncached_view(); + assert_eq!(view.write_narrow_from(0x40, 2, 0xdead_beef_1234_5678), BUS_OK); + assert_eq!(md.dma_view().read64(0x40).data >> 48, 0x5678); + } + /// Uncached CPU and DMA views report uncorrectable errors with bus status. + #[test] + fn uncached_and_dma_views_report_errors_to_the_guest() { + let md = configured(); + let r = md.md_bus(); + let e = md.ecc_bus(); + e.write8(7, 3); + assert_eq!(md.uncached_view().read64(0).status, BUS_ERR); + assert_eq!(md.dma_view().read64(0).status, BUS_OK); + assert_ne!(r.read64(0x60018).data & (1 << 60), 0); + e.write8(7, 0x13); + assert_eq!(md.ecc_scrub_bus().write64(0, 0), BUS_OK); + assert_eq!(md.physical().read64(0).data, 1); + assert_eq!(e.read8(7).data, 0x13); + assert_eq!(md.ecc_scrub_bus().write32(0, 0), BUS_ERR); + } + /// Directory reads report invalid metadata and protection faults to the guest. + #[test] + fn directory_reads_check_metadata_and_report_protection_errors() { + { + let md = configured(); + let events = Arc::new(Mutex::new(Vec::new())); + let seen = events.clone(); + let weak = Arc::downgrade(&md); + md.on_error(Arc::new(move |kind| { + assert_eq!(weak.upgrade().unwrap().current_cell(), u64::MAX); + seen.lock().push(kind); + })); + let r = md.md_bus(); + md.directory_bus().write64(0, 1); + assert_eq!(md.directory_bus().read64(0).data, 0x001f0001); + r.write64(0x40000, 2); + md.directory_bus().write64(0, 0x001e0001); + r.write64(0x40000, 0); + md.ecc_bus().write8(7, 3); + assert_eq!(md.uncached_view().read64(0).status, BUS_ERR); + assert_eq!(md.directory_bus().read64(0).data, 0x001f0001); + assert_eq!( + *events.lock(), + vec![MemoryError::Correctable, MemoryError::Uncorrectable] + ); + assert_ne!(r.read64(0x40010).data & (1 << 61), 0); + assert_ne!(r.read64(0x60010).data & (1 << 60), 0); + } + { + let md = configured(); + let d = md.directory_bus(); + assert_eq!(d.write64(0x408, 0x1234), BUS_OK); + assert_eq!(d.read64(0x2000408).data, 0x1234); + assert_eq!(d.write64(0x418, 0x5678), BUS_OK); + assert_eq!(d.read64(0x408).data, 0x1234); + assert_eq!(d.read64(0x418).data, 0x5678); + assert_eq!(d.write64(8, u64::MAX), BUS_OK); + assert_eq!(d.read64(8).data, u64::MAX); + assert_eq!(d.read32(0).status, BUS_ERR); + assert_eq!(d.read64(1 << 30).status, BUS_ERR); + } + } +} + +const DIRECTORY_PREMIUM: [u8; 64] = [ + 0x1f, 0x2f, 0x37, 0x3b, 0x3d, 0x3e, 0x07, 0x0b, 0x0d, 0x0e, 0x13, 0x15, 0x16, 0x19, 0x1a, 0x1c, + 0x7f, 0x4f, 0x57, 0x5b, 0x5d, 0x5e, 0x67, 0x6b, 0x6d, 0x6e, 0x73, 0x75, 0x76, 0x79, 0x7a, 0x7c, + 1, 2, 4, 8, 16, 32, 0x23, 0x25, 0x26, 0x29, 0x2a, 0x2c, 0x31, 0x32, 0x34, 0x38, 0x40, 0x43, + 0x45, 0x46, 0x49, 0x4a, 0x4c, 0x51, 0x52, 0x54, 0x58, 0x61, 0x62, 0x64, 0x68, 0x70, +]; +const DIRECTORY_STANDARD: [u8; 32] = [ + 0x1f, 0x2f, 0x37, 0x3b, 0x3d, 0x3e, 0x07, 0x0b, 0x0d, 0x0e, 0x13, 0x15, 0x16, 0x19, 0x1a, 0x1c, + 1, 2, 4, 8, 16, 32, 0x23, 0x25, 0x26, 0x29, 0x2a, 0x2c, 0x31, 0x32, 0x34, 0x38, +]; +fn directory_columns(premium: bool) -> &'static [u8] { + if premium { + &DIRECTORY_PREMIUM + } else { + &DIRECTORY_STANDARD + } +} +fn directory_syndrome(mut word: u64, premium: bool) -> u8 { + if !premium { + word &= u32::MAX as u64; + } + let mut check = 0; + while word != 0 { + check ^= directory_columns(premium)[word.trailing_zeros() as usize]; + word &= word - 1; + } + check +} +pub fn directory_encode(word: u64, premium: bool) -> u64 { + let word = word + & !if premium { + (63 << 32) | (1 << 48) + } else { + 63 << 16 + }; + let check = directory_syndrome(word, premium) as u64; + word | if premium { + (check & 63) << 32 | (check >> 6) << 48 + } else { + check << 16 + } +} diff --git a/src/dev/bedrock_mem.rs b/src/dev/bedrock_mem.rs new file mode 100644 index 000000000..bec848941 --- /dev/null +++ b/src/dev/bedrock_mem.rs @@ -0,0 +1,311 @@ +//! The Fuel's fitted memory: two 256 MiB banks owned by `Physical`, with +//! the bank decode selected by Bedrock's memory configuration register. +//! +//! This population follows the QEMU model; it is not a measured Fuel +//! topology. Ordinary pages answer through Physical's bank views, without +//! the MD state lock. Pages with injected ECC or marked directory lines use +//! the MD endpoint. Their entire 64 MiB region is absent from the direct +//! window, so cache fills cannot bypass those checks. +//! +//! Remapping runs on the CPU thread, or with the CPU stopped, as HEART's +//! memory descriptors do. DMA may remove diagnostic metadata but retains +//! the slow mapping until the next CPU-side rebuild. Physical publishes +//! complete decode-table pages atomically; a concurrent bus master sees an +//! old or new entry. This does not promise a transaction across a remap. +//! +//! Ported from hw/misc/sgi-bedrock-md.c in jamesbraid/qemu-sgi-origami. + +use crate::physical::{Physical, RamBank}; +use crate::traits::*; +use crate::ppmem::devmap::SLOT_SHIFT; +use crate::ppmem::{MappedMemory, BITMAP_SHIFT}; +use parking_lot::Mutex; +use std::collections::BTreeSet; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; + +pub const BANK_SPAN: u64 = 1 << 30; +pub const FITTED_BANK: u64 = 256 << 20; + +/// The PROM builds its stack and data in primary data cache lines it tags for +/// this node-local window, with no memory behind it: DRAM is not configured +/// yet. See [`BedrockMemory::startup_tag`]. +pub(crate) const STARTUP_BASE: u64 = 0x10_0000; +pub(crate) const STARTUP_SIZE: u64 = 0x8000; +/// A primary data cache tag: the state in bits 7:6, the physical address +/// bits 35:12 above bit 8. Bits above the node's 8 GiB (page bit 21) select +/// the node. +const TAG_STATE: u32 = 0xc0; +const TAG_PAGE_SHIFT: u32 = 8; +const TAG_NODE_PAGES: u32 = 1 << 21; + +#[derive(Clone, Copy, PartialEq, Eq)] +struct Run { + bank: usize, + base: u64, + size: u64, +} + +#[derive(Default)] +struct Mapping { + runs: Vec, + /// Whether the decode table's slots over the startup window hold the page. + startup_placed: bool, + // Bank-relative 4 KiB page numbers, bank 1 immediately after bank 0. + slow_pages: BTreeSet, + slow: Option>, +} + +struct NoMemory; +impl BusDevice for NoMemory {} + +/// JIT pages belong to physical addresses, not to the bank currently +/// answering there. Keep these counters separate from the banks' counters: +/// a compiled page pins its pointer across later bank reconfiguration. +#[cfg(feature = "jitv2")] +pub(crate) struct Generations(Box<[AtomicU64]>); +#[cfg(feature = "jitv2")] +impl Generations { + fn new() -> Self { + Self((0..1 << 20).map(|_| AtomicU64::new(0)).collect::>().into_boxed_slice()) + } + pub(crate) fn base(&self) -> *mut AtomicU64 { + self.0.as_ptr() as *mut AtomicU64 + } + fn bump(&self, address: u64) { + if let Some(counter) = self.0.get((address >> 12) as usize) { + counter.fetch_add(1, Ordering::Relaxed); + } + } + fn bump_all(&self) { + for slot in 0..4 { + let first = slot * (BANK_SPAN >> 12) as usize; + for counter in &self.0[first..first + (FITTED_BANK >> 12) as usize] { + counter.fetch_add(1, Ordering::Relaxed); + } + } + } +} + +pub struct BedrockMemory { + physical: Arc, + config: AtomicU64, + mapping: Mutex, + /// The window's bytes while the PROM's cache lines stand. Zero at power-on: + /// the PROM makes the lines valid without a fill. + startup_page: Box, + startup_live: AtomicBool, + #[cfg(feature = "jitv2")] + generations: Arc, +} +impl BedrockMemory { + pub fn new() -> Self { + let physical = Physical::new(vec![RamBank::new(256), RamBank::new(256)]); + physical.set_default(Arc::new(NoMemory), "Bedrock unconfigured memory"); + let startup_page = Box::new(RamBank::new(1)); + startup_page.set_addr_mask(STARTUP_SIZE as u32 - 1); + Self { physical, config: AtomicU64::new(0), mapping: Mutex::new(Mapping::default()), + startup_page, startup_live: AtomicBool::new(false), + #[cfg(feature = "jitv2")] + generations: Arc::new(Generations::new()), + } + } + pub fn physical(&self) -> &Arc { + &self.physical + } + pub(crate) fn config(&self) -> u64 { + self.config.load(Ordering::Acquire) + } + + /// Whether a primary data cache tag store starts or ends the startup + /// window, given its TagLo. PROM 6.210 stores test patterns (state bits + /// set, pages far from the window), an invalidate sweep, then one valid + /// tag for each of the window's eight pages, and a final invalidate sweep + /// once it has memory. So a valid tag for a page of the window starts it + /// and the next invalid one ends it; a kernel stores invalid tags only. + /// + /// The end is the sweep's first store, not its last. Counting accesses + /// that reach the page in an interpreted PROM 6.210 boot, none follow the + /// first of the final sweep's 2050 invalid stores (the count is the same + /// at the first and at the last), and the page is read and written + /// heavily up to that first store. Holding it to the sweep's end would + /// change nothing the PROM sees. + /// `Some(live)` is the state to move to, `None` leaves it alone. + pub(crate) fn startup_tag(&self, tag: u32) -> Option { + let live = self.startup_live.load(Ordering::Relaxed); + if tag & TAG_STATE == 0 { + return live.then_some(false); + } + let page = (tag >> TAG_PAGE_SHIFT) & (TAG_NODE_PAGES - 1); + let in_window = (STARTUP_BASE >> 12..(STARTUP_BASE + STARTUP_SIZE) >> 12).contains(&(page as u64)); + (in_window && !live).then_some(true) + } + /// Move to `live`; whether that changed anything, and so needs a remap. + pub(crate) fn set_startup(&self, live: bool) -> bool { + self.startup_live.swap(live, Ordering::Relaxed) != live + } + pub(crate) fn startup_live(&self) -> bool { + self.startup_live.load(Ordering::Relaxed) + } + /// Power-on: no cache lines stand, and the page holds zero. + pub(crate) fn clear_startup(&self) { + self.startup_live.store(false, Ordering::Relaxed); + for at in (0..STARTUP_SIZE).step_by(8) { + self.startup_page.write64(at, 0); + } + } + /// The page, for an access it answers: the window, while live. The bus + /// decode, the CPU's cached path and DMA all reach it through here or + /// through Physical's table, since nothing else is behind the window. + pub(crate) fn startup_device(&self, address: u64, bytes: u64) -> Option<&dyn BusDevice> { + let offset = address.checked_sub(STARTUP_BASE)?; + (self.startup_live() && offset + bytes <= STARTUP_SIZE) + .then(|| &*self.startup_page as &dyn BusDevice) + } + #[cfg(feature = "jitv2")] + pub(crate) fn generations(&self) -> Arc { self.generations.clone() } + #[cfg(feature = "jitv2")] + pub(crate) fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + // Disabled RAM must still have a counter: a JIT page may already + // have claimed it before the guest enables or moves a bank. + if address / BANK_SPAN < 4 && address % BANK_SPAN < FITTED_BANK { + unsafe { self.generations.base().add((address >> 12) as usize) } + } else { std::ptr::null() } + } + pub(crate) fn written(&self, _address: u64) { + #[cfg(feature = "jitv2")] + self.generations.bump(_address); + } + + /// The endpoint is weak back to MD: Physical owns it without a cycle. + pub(crate) fn set_slow_view(&self, slow: Arc) { + self.mapping.lock().slow = Some(slow); + } + + fn runs(config: u64) -> Vec { + (0..8).filter_map(|slot| { + let base = slot * BANK_SPAN; + let (bank, _) = Self::decode(config, base, 1)?; + let code = (config >> (bank / 2 * 8 + 4)) & 15; + Some(Run { bank, base, size: FITTED_BANK.min((32 << 20) << code) }) + }).collect() + } + + /// Publish a state snapshot after releasing MD's lock. Only the CPU + /// thread, or a caller with the CPU stopped, may change ppmem's bitmap. + /// A forced rebuild follows the machine setting Physical's default. + pub(crate) fn remap(&self, config: u64, slow_pages: BTreeSet, force: bool) { + let runs = Self::runs(config); + let mut old = self.mapping.lock(); + if !force && old.runs == runs && old.slow_pages == slow_pages { + self.config.store(config, Ordering::Release); + return; + } + let phys = &self.physical; + let space = phys.ppmem_space(); + // Withdraw the direct window before publishing any checked page. + if let Some(space) = space { + space.clear_mappings(); + } + // Restore only ranges previously owned by RAM. In particular, the + // unused part of a 1 GiB slot may contain flash or other devices. + if old.startup_placed { + phys.fill_slots(STARTUP_BASE >> SLOT_SHIFT, STARTUP_SIZE >> SLOT_SHIFT, phys.default_entry()); + } + for run in &old.runs { + let retained = runs.iter().find(|next| next.base == run.base) + .map_or(0, |next| next.size.min(run.size)); + if retained < run.size { + phys.fill_slots((run.base + retained) >> SLOT_SHIFT, + (run.size - retained) >> SLOT_SHIFT, phys.default_entry()); + } + } + for run in &runs { + phys.fill_slots(run.base >> SLOT_SHIFT, run.size >> SLOT_SHIFT, phys.bank_view(run.bank, run.base)); + let bank_page = run.bank as u64 * (FITTED_BANK >> SLOT_SHIFT); + let pages = run.size >> SLOT_SHIFT; + let mut checked_regions = BTreeSet::new(); + for &page in slow_pages.range(bank_page..bank_page + pages) { + let offset = (page - bank_page) << SLOT_SHIFT; + let address = run.base + offset; + checked_regions.insert(offset >> BITMAP_SHIFT); + phys.map(address, address + (1 << SLOT_SHIFT), + old.slow.as_ref().expect("Bedrock MD slow endpoint").clone(), "Bedrock checked RAM"); + } + if let Some(space) = space { + let region_size = 1 << BITMAP_SHIFT; + for offset in (0..run.size).step_by(region_size as usize) { + // The startup page answers through the decode only, so the + // region holding it stays out of the direct window. + let startup = self.startup_live() && run.base + offset == 0; + if !startup && !checked_regions.contains(&(offset >> BITMAP_SHIFT)) { + if let Err(error) = space.map_range(run.bank, offset, run.base + offset, region_size) { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock RAM window at {:#x}: {error}", run.base + offset); + } + } + } + } + } + let live = self.startup_live(); + if live { + let page: *const RamBank = &*self.startup_page; + phys.fill_slots(STARTUP_BASE >> SLOT_SHIFT, STARTUP_SIZE >> SLOT_SHIFT, + page as crate::ppmem::devmap::Entry); + } + old.startup_placed = live; + self.config.store(config, Ordering::Release); + // Publish changed ownership before advancing the stable counters, + // so an in-flight compile cannot accept bytes from the old decode. + self.changed_decode(); + old.runs = runs; + old.slow_pages = slow_pages; + } + + pub(crate) fn decode(config: u64, addr: u64, bytes: u64) -> Option<(usize, u64)> { + let software = addr / BANK_SPAN; + if software >= 8 { + return None; + } + let bank = software ^ ((config >> 32) & 3); + let field = config >> (bank / 2 * 8); + let code = (field >> 4) & 15; + if bank >= 2 || field & (1 << (bank & 1)) == 0 || !(1..=5).contains(&code) { + return None; + } + let limit = FITTED_BANK.min((32 << 20) << code); + let offset = addr % BANK_SPAN; + (offset < limit && bytes <= limit - offset).then_some((bank as usize, offset)) + } + pub(crate) fn raw_read(&self, bank: usize, at: u64) -> u64 { + self.physical.banks()[bank].read64(at).data + } + pub(crate) fn raw_write(&self, bank: usize, at: u64, value: u64) -> u32 { + let result = self.physical.banks()[bank].write64(at, value); + if result == BUS_OK { + let slot = bank as u64 ^ ((self.config() >> 32) & 3); + self.written(slot * BANK_SPAN + at); + } + result + } + pub(crate) fn zero(&self, bank: usize, size: u64) { + assert!(size <= FITTED_BANK); + // The bank owns this contiguous mapping; zero has the same representation + // in host word order. Self-test does not synchronize concurrent bank accesses. + unsafe { + std::ptr::write_bytes(self.physical.banks()[bank].host_ptr(), 0, size as usize); + } + #[cfg(feature = "jitv2")] + self.physical.banks()[bank].bump_gen_all(); + #[cfg(feature = "jitv2")] + self.generations.bump_all(); + } + pub(crate) fn changed_decode(&self) { + #[cfg(feature = "jitv2")] + self.generations.bump_all(); + #[cfg(feature = "jitv2")] + for bank in self.physical.banks() { + bank.bump_gen_all(); + } + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index d8f7d6b72..013e5b626 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,6 +60,8 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod bedrock_md; +pub mod bedrock_mem; pub mod dp83843; pub mod ics1890; pub mod isp12160; From 529216f0aec9b90b79781d3f03d114da439295ff Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 10/20] bedrock: add the node I/O, network, crossbar and local-bus blocks I/O (bedrock_io): Fuel needs Bedrock I/O to route programmed PIO windows to PCI widgets, check inbound DMA permissions and run its two block-transfer engines. Fault reporting and interrupt delivery stay with the block that owns the state, and unsupported accesses are reported. Register map and behaviour are ported from the QEMU model. Node routing (bedrock_ni): Fuel runs as one node with no NUMAlink peer, but the guest still reads and programs these local control words during machine setup. Retain the writes and keep the network fabric out of the model. Crossbar (bedrock_xb): Record the fabric error queues and let software service them, with the outputs available to the Bedrock processor interface. Unsupported queue reads return zero and writes are logged and ignored. Local bus (bedrock_lb): Add the node and region registers, atomic scratch aliases, error and vector status, and the MicroLAN master that board identity parts sit on. The tests drive it together with the I/O, node-interface and crossbar blocks it reports through, which land in this same commit. --- src/dev/bedrock_io.rs | 811 ++++++++++++++++++++++++++++++++++++++++++ src/dev/bedrock_lb.rs | 516 +++++++++++++++++++++++++++ src/dev/bedrock_ni.rs | 335 +++++++++++++++++ src/dev/bedrock_xb.rs | 278 +++++++++++++++ src/dev/mod.rs | 4 + 5 files changed, 1944 insertions(+) create mode 100644 src/dev/bedrock_io.rs create mode 100644 src/dev/bedrock_lb.rs create mode 100644 src/dev/bedrock_ni.rs create mode 100644 src/dev/bedrock_xb.rs diff --git a/src/dev/bedrock_io.rs b/src/dev/bedrock_io.rs new file mode 100644 index 000000000..4b3e659c3 --- /dev/null +++ b/src/dev/bedrock_io.rs @@ -0,0 +1,811 @@ +//! Bedrock IO routes programmed PIO windows to PCI widgets, applies inbound +//! permission checks to DMA requests and exposes two block-transfer engines. +//! Guests configure the widget masks before touching devices; the BTE moves +//! 128-byte lines and writes completion state through the configured target. +//! +//! # What is approximated here +//! +//! BTE transfers and their completion callbacks run synchronously when a +//! start is written. Timing and unpublished register behavior are not modeled. +//! +//! The plain register words sit in a sparse map keyed by offset, since the +//! 0x30010-byte block holds only a few dozen of them. +//! +//! Ported from hw/misc/sgi-bedrock-io.c in jamesbraid/qemu-sgi-origami. + +/// Length of the block-relative register aperture. +pub const SIZE: u64 = 0x30010; + +/// The public Linux Hub I/O protection-override key literal. +/// +/// Linux `arch/mips/include/asm/sn/sn0/hubio.h` at +/// `db2ddb87143519e20a95aa36c60b36107b736a58` defines +/// `IIO_PROTECT_OVRRD_KEY` as this value (lines 31, 47). This establishes +/// the literal's provenance only; it does not establish a register mapping. +pub const IIO_PROTECT_OVRRD_KEY: u64 = 0x53474972756c6573; + +pub mod reg { + pub const DEVICE_ID: u64 = 0x0; + pub const PROTECTION_KEY: u64 = 0x108; + pub const PIO_PERMISSION: u64 = 0x100; + pub const INBOUND_PERMISSION: u64 = 0x110; + pub const INBOUND_DEVICE_PERMISSION: u64 = 0x120; + pub const XIO_STATUS: u64 = 0x128; + pub const LINK_LOG: u64 = 0x130; + pub const LINK_STATUS: u64 = 0x138; + pub const BTE1_BASE: u64 = 0x10000; + pub const BTE_REGISTER_SIZE: u64 = 0x30; + pub const READ_ONLY: u64 = 0x8; + pub const BTE2_BASE: u64 = 0x20000; + pub const WIDGET_STATUS: u64 = 0x20; + pub const WIDGET_PERMISSION: u64 = 0x118; + pub const REGISTER_0150: u64 = 0x150; + pub const REGISTER_0158: u64 = 0x158; + pub const ITTE_BASE: u64 = 0x160; + pub const ITTE_LAST: u64 = 0x190; + pub const REGISTER_0198_BASE: u64 = 0x198; + pub const REGISTER_01D8_LAST: u64 = 0x1d8; + pub const READ_ONLY_ALIAS: u64 = 0x1f8; + pub const REGISTER_01E0: u64 = 0x1e0; + pub const REGISTER_01E8: u64 = 0x1e8; + pub const REGISTER_01F0: u64 = 0x1f0; + pub const REGISTER_0200: u64 = 0x200; + pub const REGISTER_03A8: u64 = 0x3a8; + pub const REGISTER_03B0: u64 = 0x3b0; + pub const REGISTER_03B8: u64 = 0x3b8; + pub const REGISTER_03C0: u64 = 0x3c0; + pub const RETAINED_WINDOW_BASE: u64 = 0x308; + pub const RETAINED_WINDOW_LAST: u64 = 0x348; + pub const RETAINED_CONFIG_BASE: u64 = 0x400; + pub const RETAINED_CONFIG_LAST: u64 = 0x5d8; + pub const GFX_MAP_BASE: u64 = 0x140; +} +pub mod bte_reg { + pub const STATUS: u64 = 0x0; + pub const SOURCE: u64 = 0x8; + pub const DESTINATION: u64 = 0x10; + pub const CONTROL: u64 = 0x18; + pub const NOTIFICATION: u64 = 0x20; + pub const INTERRUPT: u64 = 0x28; +} + +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::collections::{BTreeMap, HashSet}; +use std::sync::Arc; +use std::thread::ThreadId; +const ID: u64 = (2 << 28) | (0xc110 << 12) | (0x36 << 1); +const ADDR_MASK: u64 = 0xffffffff80; +const BUSY: u64 = 1 << 20; +const ERROR: u64 = 1 << 16; +#[derive(Clone, Copy, Default)] +struct Bte { + status: u64, + source: u64, + destination: u64, + control: u64, + notification: u64, + interrupt: u64, +} +struct Regs { + words: BTreeMap, + bte: [Bte; 2], + node: u8, + peer: bool, +} +impl Default for Regs { + fn default() -> Self { + let mut words = BTreeMap::new(); + for (a, v) in [ + (0x20, 0xa), + (0x100, !0), + (0x110, !0), + (0x118, !0), + (0x120, !0), + (0x128, 0x3fc0fff6403 & !(1 << 13)), + (0x1e8, 0x111), + ] { + words.insert(a, v); + } + Self { + words, + bte: [Bte::default(); 2], + node: 0, + peer: false, + } + } +} +// Only a synchronous callback on the same thread is recursive. DMA workers +// on other threads may call the thread-safe downstream bus independently. +struct InboundAccess<'a> { + active: &'a Mutex>, + thread: ThreadId, +} +impl Drop for InboundAccess<'_> { + fn drop(&mut self) { + self.active.lock().remove(&self.thread); + } +} + +pub struct BedrockIo { + inbound_threads: Mutex>, + regs: Arc>, + widgets: Mutex<[Option>; 16]>, + memory: Mutex>>, + hub: Mutex>>, +} +impl Default for BedrockIo { + fn default() -> Self { + Self::new() + } +} +impl BedrockIo { + pub fn new() -> Self { + let regs = Arc::new(Mutex::new(Regs::default())); + crate::locks::register_mutex("bedrock-io", ®s); + Self { + regs, + inbound_threads: Mutex::new(HashSet::new()), + widgets: Mutex::new(std::array::from_fn(|_| None)), + memory: Mutex::new(None), + hub: Mutex::new(None), + } + } + pub fn connect_widget(&self, widget: u8, dev: Arc) { + assert!(widget < 16); + self.widgets.lock()[widget as usize] = Some(dev); + self.set_xio_peer(true); + } + pub fn set_xio_peer(&self, peer: bool) { + let mut r = self.regs.lock(); + r.peer = peer; + if peer { + *r.words.entry(reg::XIO_STATUS).or_default() |= 1 << 13; + } else { + *r.words.entry(reg::XIO_STATUS).or_default() &= !(1 << 13); + } + } + pub fn connect_memory(&self, dev: Arc) { + *self.memory.lock() = Some(dev); + } + pub fn connect_hub(&self, dev: Arc) { + *self.hub.lock() = Some(dev); + } + pub fn set_node_number(&self, node: u8) { + self.regs.lock().node = node; + } + pub fn gfx_map(&self, entry: usize) -> u64 { + assert!(entry < 2); + self.word(reg::GFX_MAP_BASE + entry as u64 * 8) + } + fn word(&self, a: u64) -> u64 { + self.regs.lock().words.get(&a).copied().unwrap_or(0) + } + pub fn disable_device(&self, widget: u8, device: u8) { + if (8..16).contains(&widget) && device < 8 { + *self.regs.lock().words.entry(reg::INBOUND_DEVICE_PERMISSION).or_default() &= + !(1 << ((widget - 8) * 8 + device)); + } + } + fn permitted(r: &Regs, widget: u8, device: u8) -> bool { + widget < 16 + && device < 8 + && r.words.get(®::WIDGET_PERMISSION).copied().unwrap_or(0) & (1 << widget) != 0 + && (widget < 8 + || r.words.get(®::INBOUND_DEVICE_PERMISSION).copied().unwrap_or(0) & (1 << ((widget - 8) * 8 + device)) + != 0) + } + fn retained(a: u64) -> bool { + matches!( + a, + 0x208 + | 0x210 + | 0x218 + | 0x220 + | 0x228 + | 0x300 + | reg::GFX_MAP_BASE + | 0x148 + | 0x388 + | 0x390 + | 0x398 + | 0x3a0 + | 0x600 + | 0x608 + | 0x610 + | 0x30000 + | 0x30008 + ) || (reg::RETAINED_WINDOW_BASE..reg::RETAINED_WINDOW_LAST).contains(&a) + || (reg::RETAINED_CONFIG_BASE..=reg::RETAINED_CONFIG_LAST).contains(&a) + } + fn mask(a: u64) -> Option { + Some(match a { + reg::WIDGET_STATUS => 0x7fe01f, + reg::PIO_PERMISSION | reg::INBOUND_PERMISSION | reg::WIDGET_PERMISSION | reg::INBOUND_DEVICE_PERMISSION | reg::REGISTER_0150 | reg::REGISTER_0158 => !0, + reg::XIO_STATUS => 0x3ff0fff7d3f, + reg::LINK_STATUS => 0x101ff7f, + reg::ITTE_BASE..=reg::ITTE_LAST => 0x1f1f, + reg::REGISTER_0198_BASE..=reg::REGISTER_01D8_LAST => !0, + reg::REGISTER_01E0 => 0x3ffffff, + reg::REGISTER_01E8 => 0x1111, + reg::REGISTER_01F0 => 0x0fffffff03ffffff, + reg::REGISTER_0200 => 0x10f, + reg::REGISTER_03A8 => 0x13ffffffff30f3, + reg::REGISTER_03B0 => 0x1ff0f, + reg::REGISTER_03B8 => 0xff, + reg::REGISTER_03C0 => 0xffffff, + _ if Self::retained(a) => !0, + _ => return None, + }) + } + fn bte_at(a: u64) -> Option<(usize, u64)> { + if (reg::BTE1_BASE..reg::BTE1_BASE + reg::BTE_REGISTER_SIZE).contains(&a) { + Some((0, a - reg::BTE1_BASE)) + } else if (reg::BTE2_BASE..reg::BTE2_BASE + reg::BTE_REGISTER_SIZE).contains(&a) { + Some((1, a - reg::BTE2_BASE)) + } else { + None + } + } + fn target(&self, offset: u64) -> Option<(u8, u64)> { + if offset < 0x10000000 { + Some(((offset >> 24) as u8, offset & 0xffffff)) + } else if offset >= 0x20000000 { + let entry = (offset >> 29) - 1; + if entry >= 7 { + return None; + } + let itte = self.word(reg::ITTE_BASE + entry * 8); + Some(( + ((itte >> 8) & 15) as u8, + ((itte & 31) << 29) + | (offset & 0x1fffffff) + | if itte & (1 << 12) != 0 { 1 << 47 } else { 0 }, + )) + } else { + None + } + } + pub fn pio_read(&self, offset: u64, size: u8) -> BusRead64 { + self.pio(offset, size, None) + } + pub fn pio_write(&self, offset: u64, size: u8, value: u64) -> u32 { + self.pio(offset, size, Some(value)).status + } + fn pio(&self, offset: u64, size: u8, value: Option) -> BusRead64 { + let Some((widget, target)) = self.target(offset) else { + return BusRead64::err(); + }; + { + let r = self.regs.lock(); + if r.words.get(&0x100).copied().unwrap_or(0) & 1 == 0 + || r.words.get(&0x110).copied().unwrap_or(0) & (1 << widget) == 0 + { + return BusRead64::err(); + } + } + if !matches!(size, 1 | 2 | 4 | 8) { + return BusRead64::err(); + } + if widget == 1 && target == 0 { + return BusRead64::ok(ID); + } + let dev = self.widgets.lock()[widget as usize].clone(); + Self::access(dev, target, size, value) + } + /// Device None means every possible DMA master must be permitted. + pub fn inbound_read( + &self, + widget: u8, + device: Option, + address: u64, + size: u8, + ) -> BusRead64 { + self.inbound(widget, device, address, size, None) + } + pub fn inbound_write( + &self, + widget: u8, + device: Option, + address: u64, + size: u8, + value: u64, + ) -> u32 { + self.inbound(widget, device, address, size, Some(value)) + .status + } + fn inbound( + &self, + widget: u8, + device: Option, + address: u64, + size: u8, + value: Option, + ) -> BusRead64 { + if address >= (1 << 48) || size as u64 > (1 << 48) - address { + return BusRead64::err(); + } + { + let r = self.regs.lock(); + let allowed = if let Some(device) = device { + Self::permitted(&r, widget, device) + } else { + (0..8).all(|d| Self::permitted(&r, widget, d)) + }; + if !allowed { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock IO inbound access refused from widget {widget} device {device:?}" + ); + return BusRead64::err(); + } + } + let thread = std::thread::current().id(); + if !self.inbound_threads.lock().insert(thread) { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO reentrant inbound packet refused"); + return BusRead64::err(); + } + let _access = InboundAccess { + active: &self.inbound_threads, + thread, + }; + if address & (1 << 47) != 0 { + let node = ((address >> 39) & 255) as u8; + let window = address & ((1 << 39) - 1); + if node != self.regs.lock().node || window & !0x7fffff != 0x1800000 { + return BusRead64::err(); + } + let dev = self.hub.lock().clone(); + Self::access(dev, window & 0x7fffff, size, value) + } else { + let dev = self.memory.lock().clone(); + Self::access(dev, address, size, value) + } + } + fn access(dev: Option>, a: u64, size: u8, value: Option) -> BusRead64 { + let Some(dev) = dev else { + return BusRead64::err(); + }; + if let Some(v) = value { + let status = match size { + 1 => dev.write8(a, v as u8), + 2 => dev.write16(a, v as u16), + 4 => dev.write32(a, v as u32), + 8 => dev.write64(a, v), + _ => BUS_ERR, + }; + BusRead64 { status, data: 0 } + } else { + match size { + 1 => { + let r = dev.read8(a); + BusRead64 { + status: r.status, + data: r.data as u64, + } + } + 2 => { + let r = dev.read16(a); + BusRead64 { + status: r.status, + data: r.data as u64, + } + } + 4 => { + let r = dev.read32(a); + BusRead64 { + status: r.status, + data: r.data as u64, + } + } + 8 => dev.read64(a), + _ => BusRead64::err(), + } + } + } + fn run_bte(&self, n: usize, mut b: Bte) { + let memory = self.memory.lock().clone(); + let count = b.status & 0xffff; + let bytes = count * 128; + let overflow = (b.control & 1 == 0 && b.source > (1 << 40) - bytes) + || b.destination > (1 << 40) - bytes; + let mut remaining = count; + if overflow { + b.status = remaining | ERROR; + } else { + while remaining != 0 { + let mut line = [0u64; 16]; + let mut status = BUS_OK; + if b.control & 1 == 0 { + for (i, v) in line.iter_mut().enumerate() { + let r = Self::access(memory.clone(), b.source + i as u64 * 8, 8, None); + status = r.status; + if status != BUS_OK { + break; + } + *v = r.data; + } + } + if status == BUS_OK { + for (i, v) in line.into_iter().enumerate() { + status = + Self::access(memory.clone(), b.destination + i as u64 * 8, 8, Some(v)) + .status; + if status != BUS_OK { + break; + } + } + } + if status != BUS_OK { + break; + } + remaining -= 1; + b.source = (b.source + 128) & ADDR_MASK; + b.destination = (b.destination + 128) & ADDR_MASK; + } + b.status = if remaining == 0 { 0 } else { remaining | ERROR }; + } + self.regs.lock().bte[n] = b; + if b.control & 0x10 != 0 { + if Self::access(memory, b.notification, 8, Some(b.status)).status != BUS_OK { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO BTE notification write failed"); + } + } else { + let node = ((b.interrupt >> 1) & 255) as u8; + if node != self.regs.lock().node { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO BTE interrupt reaches no node"); + return; + } + let hub = self.hub.lock().clone(); + let offset = if b.interrupt & 1 != 0 { 0x200090 } else { 0x90 }; + if Self::access(hub, offset, 8, Some(0x100 | ((b.interrupt >> 16) & 127))).status + != BUS_OK + { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO BTE completion interrupt failed"); + } + } + } +} +impl Resettable for BedrockIo { + fn power_on(&self) { + let mut r = self.regs.lock(); + let peer = r.peer; + let node = r.node; + *r = Regs::default(); + r.node = node; + r.peer = peer; + if peer { + *r.words.entry(reg::XIO_STATUS).or_default() |= 1 << 13; + } + } +} +impl BusDevice for BedrockIo { + fn read64(&self, a: u64) -> BusRead64 { + if a & 7 != 0 { + return BusRead64::err(); + } + let mut r = self.regs.lock(); + if let Some((n, a)) = Self::bte_at(a) { + let b = &mut r.bte[n]; + return BusRead64::ok(match a { + bte_reg::STATUS => b.status, + bte_reg::SOURCE => b.source, + bte_reg::DESTINATION => b.destination, + bte_reg::CONTROL => { + b.status &= !BUSY; + b.control + } + bte_reg::NOTIFICATION => b.notification, + bte_reg::INTERRUPT => b.interrupt, + _ => 0, + }); + } + let v = match a { + reg::DEVICE_ID => ID, + reg::READ_ONLY | 0x1f8 => 0, + reg::LINK_STATUS => r.words.get(&a).copied().unwrap_or(0) & !(1 << 28), + reg::LINK_LOG => r.words.get(&a).copied().unwrap_or(0), + _ if Self::mask(a).is_some() => r.words.get(&a).copied().unwrap_or(0), + _ => { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO unsupported read {a:#x}"); + 0 + } + }; + BusRead64::ok(v) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + let mut r = self.regs.lock(); + if let Some((n, offset)) = Self::bte_at(a) { + let b = &mut r.bte[n]; + let mut run = false; + match offset { + bte_reg::STATUS => b.status = v & (0xffff | BUSY), + bte_reg::SOURCE => b.source = v & ADDR_MASK, + bte_reg::DESTINATION => b.destination = v & ADDR_MASK, + bte_reg::CONTROL => { + b.control = v & 0x111; + if b.control & 0x100 != 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO BTE poison unsupported"); + } else if b.status & BUSY != 0 { + if b.status & 0xffff == 0 { + b.status = 0; + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO zero-count BTE start"); + } else { + run = true; + } + } + } + bte_reg::NOTIFICATION => b.notification = v & ADDR_MASK, + bte_reg::INTERRUPT => b.interrupt = v & 0x7f01ff, + _ => {} + } + let b = *b; + drop(r); + if run { + self.run_bte(n, b); + } + return BUS_OK; + } + match a { + reg::PROTECTION_KEY => { + if v == IIO_PROTECT_OVRRD_KEY { + *r.words.entry(0x100).or_default() |= 1; + } + } + reg::INBOUND_DEVICE_PERMISSION | reg::REGISTER_01E8 => { + *r.words.entry(a).or_default() |= v & Self::mask(a).unwrap(); + } + reg::XIO_STATUS => { + let v = v & Self::mask(a).unwrap() & if r.peer { !0 } else { !(1 << 13) }; + r.words.insert(a, v); + } + reg::LINK_LOG => { + if v == 0 { + r.words.insert(a, 0); + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO nonzero link-log write ignored"); + } + } + reg::READ_ONLY_ALIAS | reg::DEVICE_ID | reg::READ_ONLY => { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO read-only or unsupported clear write {a:#x}"); + } + _ => { + if let Some(mask) = Self::mask(a) { + r.words.insert(a, v & mask); + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock IO unsupported write {a:#x} = {v:#x}"); + } + } + } + BUS_OK + } +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sn1-bedrock-io-test.c in + // jamesbraid/qemu-sgi-origami: inbound-protection, bte and + // bte-error-termination. + use super::*; + #[derive(Default)] + struct Memory { + words: Mutex>, + fail_read: Mutex>, + fail_write: Mutex>, + } + impl BusDevice for Memory { + fn read64(&self, a: u64) -> BusRead64 { + if *self.fail_read.lock() == Some(a) { + return BusRead64::err(); + } + BusRead64::ok(self.words.lock().get(&a).copied().unwrap_or(0)) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if *self.fail_write.lock() == Some(a) { + return BUS_ERR; + } + self.words.lock().insert(a, v); + BUS_OK + } + } + fn launch(d: &BedrockIo, base: u64, count: u64, src: u64, dst: u64, notify: u64, control: u64) { + d.write64(base, count | BUSY); + d.write64(base + 8, src); + d.write64(base + 0x10, dst); + d.write64(base + 0x20, notify); + d.write64(base + 0x18, control); + } + struct Reentrant(std::sync::Weak); + impl BusDevice for Reentrant { + fn read64(&self, a: u64) -> BusRead64 { + self.0.upgrade().unwrap().inbound_read(15, Some(0), a, 8) + } + } + struct Busy; + impl BusDevice for Busy { + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::busy() + } + fn write64(&self, _: u64, _: u64) -> u32 { + crate::traits::BUS_BUSY + } + } + struct BlockingMemory { + entered: std::sync::mpsc::Sender<()>, + release: Mutex>, + words: Mutex>, + } + impl BusDevice for BlockingMemory { + fn write64(&self, address: u64, value: u64) -> u32 { + if address == 0x1000 { + self.entered.send(()).unwrap(); + self.release + .lock() + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + } + self.words.lock().insert(address, value); + BUS_OK + } + } + + /// The guest must enable widget and requester masks before PIO or inbound DMA succeeds. + #[test] + fn the_io_block_checks_pio_and_inbound_requester_permissions() { + { + let d = BedrockIo::new(); + let m = Arc::new(Memory::default()); + d.connect_widget(8, m.clone()); + d.write64(reg::ITTE_BASE, 0x802); + assert_eq!(d.pio_write(0x20000080, 8, 0xabcd), BUS_OK); + assert_eq!(m.read64(0x40000080).data, 0xabcd); + assert_eq!(d.pio_read(0x20000080, 8).data, 0xabcd); + d.write64(reg::INBOUND_PERMISSION, 0); + assert_eq!(d.pio_read(0x20000080, 8).status, BUS_ERR); + d.write64(reg::INBOUND_PERMISSION, !0); + d.write64(reg::PIO_PERMISSION, 0); + assert_eq!(d.pio_read(0x20000080, 8).status, BUS_ERR); + d.write64(reg::PROTECTION_KEY, IIO_PROTECT_OVRRD_KEY); + assert_eq!(d.pio_read(0x20000080, 8).status, BUS_OK); + assert_eq!(d.pio_read(0x10000000, 8).status, BUS_ERR); + d.power_on(); + assert_eq!(d.read64(reg::PIO_PERMISSION).data, !0); + assert_eq!(d.read64(reg::REGISTER_01E8).data, 0x111); + + } + { + let d = BedrockIo::new(); + let m = Arc::new(Memory::default()); + let h = Arc::new(Memory::default()); + d.connect_memory(m.clone()); + d.connect_hub(h.clone()); + assert_eq!(d.inbound_write(14, Some(2), 0x80, 8, 42), BUS_OK); + assert_eq!(m.read64(0x80).data, 42); + d.disable_device(14, 2); + assert_eq!(d.inbound_read(14, Some(2), 0x80, 8).status, BUS_ERR); + assert_eq!(d.inbound_read(14, None, 0x80, 8).status, BUS_ERR); + assert_eq!(d.inbound_read(14, Some(1), 0x80, 8).status, BUS_OK); + let a = (1 << 47) | 0x1800090; + assert_eq!(d.inbound_write(15, Some(0), a, 8, 0x123), BUS_OK); + assert_eq!(h.read64(0x90).data, 0x123); + assert_eq!( + d.inbound_read(15, Some(0), a | (1 << 39), 8).status, + BUS_ERR + ); + assert_eq!(d.inbound_read(15, Some(0), 1 << 48, 8).status, BUS_ERR); + assert_eq!(d.inbound_read(15, Some(0), a + 0x800000, 8).status, BUS_ERR); + } + } + /// The guest observes BTE line copies and zero-fill through its completion registers. + #[test] + fn the_bte_copies_and_zeroes_lines_before_completion() { + let d = BedrockIo::new(); + let m = Arc::new(Memory::default()); + let h = Arc::new(Memory::default()); + d.connect_memory(m.clone()); + d.connect_hub(h.clone()); + for i in 0..32 { + m.write64(0x1000 + i * 8, 0xfeed0000 | i); + } + launch(&d, reg::BTE1_BASE, 2, 0x1000, 0x2000, 0x3000, 0x10); + for i in 0..32 { + assert_eq!(m.read64(0x2000 + i * 8).data, 0xfeed0000 | i); + } + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 0); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::SOURCE).data, 0x1100); + assert_eq!(m.read64(0x3000).data, 0); + d.write64(reg::BTE2_BASE + bte_reg::INTERRUPT, 53 << 16); + launch(&d, reg::BTE2_BASE, 1, 0, 0x2000, 0, 1); + for i in 0..16 { + assert_eq!(m.read64(0x2000 + i * 8).data, 0); + } + assert_eq!(h.read64(0x90).data, 0x135); + } + /// A failed BTE line stops the transfer and reports the failing source and destination. + #[test] + fn the_bte_reports_transfer_errors_and_stops_after_a_failed_line() { + { + for (read, fail) in [(true, 0x1080), (false, 0x2080)] { + let d = BedrockIo::new(); + let m = Arc::new(Memory::default()); + d.connect_memory(m.clone()); + if read { + *m.fail_read.lock() = Some(fail); + } else { + *m.fail_write.lock() = Some(fail); + } + launch(&d, reg::BTE1_BASE, 3, 0x1000, 0x2000, 0x3000, 0x10); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 2 | ERROR); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::SOURCE).data, 0x1080); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::DESTINATION).data, 0x2080); + assert_eq!(m.read64(0x3000).data, 2 | ERROR); + } + let d = BedrockIo::new(); + let m = Arc::new(Memory::default()); + d.connect_memory(m.clone()); + *m.fail_write.lock() = Some(0x3000); + launch(&d, reg::BTE1_BASE, 1, 0x1000, 0x2000, 0x3000, 0x10); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 0); + launch(&d, reg::BTE1_BASE, 2, 0xffffffff80, 0x2000, 0x3000, 0x10); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 2 | ERROR); + } + { + let d = BedrockIo::new(); + launch(&d, reg::BTE1_BASE, 1, 0, 0, 0, 0x100); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 1 | BUSY); + d.read64(reg::BTE1_BASE + bte_reg::CONTROL); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 1); + launch(&d, reg::BTE1_BASE, 0, 0, 0, 0, 0x10); + assert_eq!(d.read64(reg::BTE1_BASE + bte_reg::STATUS).data, 0); + } + } + /// A downstream callback may reenter the IO block while an inbound transaction is active. + #[test] + fn reentrant_inbound_forwards_are_refused_and_can_be_retried() { + let d = Arc::new(BedrockIo::new()); + d.connect_memory(Arc::new(Reentrant(Arc::downgrade(&d)))); + assert_eq!(d.inbound_read(15, Some(0), 0, 8).status, BUS_ERR); + d.connect_memory(Arc::new(Busy)); + assert_eq!( + d.inbound_read(15, Some(0), 0, 8).status, + crate::traits::BUS_BUSY + ); + d.connect_widget(8, Arc::new(Busy)); + assert_eq!(d.pio_read(0x8000000, 8).status, crate::traits::BUS_BUSY); + assert_eq!(d.pio_write(0x8000000, 8, 42), crate::traits::BUS_BUSY); + + } + /// The two guest BTEs can reach node memory without one worker holding the other off. + #[test] + fn independent_bte_workers_can_reach_memory_concurrently() { + let d = Arc::new(BedrockIo::new()); + let (entered_tx, entered_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = std::sync::mpsc::channel(); + let memory = Arc::new(BlockingMemory { + entered: entered_tx, + release: Mutex::new(release_rx), + words: Mutex::new(BTreeMap::new()), + }); + d.connect_memory(memory.clone()); + let first_io = d.clone(); + let first = + std::thread::spawn(move || first_io.inbound_write(14, Some(0), 0x1000, 8, 0x1234)); + entered_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + let second_io = d.clone(); + let second = + std::thread::spawn(move || second_io.inbound_write(15, Some(1), 0x2000, 8, 0x5678)); + let second_status = second.join().unwrap(); + release_tx.send(()).unwrap(); + let first_status = first.join().unwrap(); + assert_eq!(second_status, BUS_OK); + assert_eq!(first_status, BUS_OK); + assert_eq!( + *memory.words.lock(), + BTreeMap::from([(0x1000, 0x1234), (0x2000, 0x5678)]) + ); + } +} diff --git a/src/dev/bedrock_lb.rs b/src/dev/bedrock_lb.rs new file mode 100644 index 000000000..7f3d86b3d --- /dev/null +++ b/src/dev/bedrock_lb.rs @@ -0,0 +1,516 @@ +//! Bedrock LB provides the node and region registers, atomic scratch aliases, +//! error and vector status, and the MicroLAN master used by board identity +//! parts. The guest polls the MicroLAN completion bit after issuing a slot. +//! +//! # What is approximated here +//! +//! The slot is decoded immediately and completes after one microsecond of +//! machine time. Permission registers retain guest writes, but this block's +//! access interface does not carry requester identity, so it cannot enforce +//! those permissions. +//! +//! The plain register words sit in a sparse map keyed by offset, since the +//! 0xc040-byte block holds only a few dozen of them. +//! +//! Ported from hw/misc/sgi-bedrock-lb.c in jamesbraid/qemu-sgi-origami. + +/// Length of the block-relative register aperture. +pub const SIZE: u64 = 0xc040; + +pub mod reg { + pub const NODE_REGION: u64 = 0x0; + pub const PIO_PERMISSION: u64 = 0x4000; + pub const PIO_PERMISSION_KEY: u64 = 0x4008; + pub const INBOUND_PERMISSION: u64 = 0x4010; + pub const SOFT_RESET: u64 = 0x4018; + pub const MICRO_LAN: u64 = 0x4030; + pub const ERROR_STATUS: u64 = 0x4040; + pub const ERROR_CLEAR: u64 = 0x4048; + pub const PIO_REGION_MASK: u64 = 0x4028; + pub const NODE_REGION_CONFIG: u64 = 0x4050; + pub const NODE_REGION_MASK: u64 = 0x4058; + pub const PIO_CONFIG: u64 = 0x4100; + pub const INBOUND_CONFIG: u64 = 0x4200; + pub const INBOUND_MASK: u64 = 0x4208; + pub const VECTOR_REQUEST: u64 = 0xc000; + pub const VECTOR_STATUS: u64 = 0xc020; + pub const VECTOR_STATUS_ALIAS: u64 = 0xc038; + pub const PIO_WINDOW: u64 = 0x8000; + pub const PIO_WINDOW_END: u64 = 0x8020; + pub const ATOMIC_SCRATCH: u64 = 0x8040; + pub const ATOMIC_SCRATCH_END: u64 = 0x8050; + pub const ATOMIC_READ0: u64 = 0x8058; + pub const ATOMIC_READ1: u64 = 0x8060; + pub const REGISTER_4020: u64 = 0x4020; + pub const REGISTER_4060: u64 = 0x4060; + pub const REGISTER_C008: u64 = 0xc008; + pub const REGISTER_C010: u64 = 0xc010; + pub const REGISTER_C028: u64 = 0xc028; + pub const REGISTER_C030: u64 = 0xc030; +} + +use crate::dev::bedrock_io::IIO_PROTECT_OVRRD_KEY; +use crate::dev::w1; +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::{Mutex, ReentrantMutex}; +use std::collections::BTreeMap; +use std::sync::Arc; +pub type Clock = Arc u64 + Send + Sync>; +type Level = Arc; +const ID: u64 = (0x36 << 1) | (0x3012 << 12) | (2 << 28); +const VECTOR_MASK: u64 = 3 | (0x1fffff << 3) | (0xff << 32) | (0x7ff << 40); +struct Regs { + words: BTreeMap, + scratch: [u64; 5], + node: u8, + region: u8, + deadline: Option, + sample: bool, +} +impl Default for Regs { + fn default() -> Self { + let mut words = BTreeMap::new(); + words.insert(0x4000, !0); + words.insert(0x4010, !0); + Self { + words, + scratch: [0; 5], + node: 0, + region: 0, + deadline: None, + sample: true, + } + } +} +pub struct BedrockLb { + regs: Arc>, + clock: Clock, + error: Mutex>, + delivery: ReentrantMutex<()>, + driven: Mutex>, + reset: Mutex>>, + microlan: Mutex>, + node_changed: Mutex>>, +} +impl BedrockLb { + pub fn new(clock: Clock) -> Self { + let regs = Arc::new(Mutex::new(Regs::default())); + crate::locks::register_mutex("bedrock-lb", ®s); + Self { + regs, + clock, + error: Mutex::new(None), + delivery: ReentrantMutex::new(()), + driven: Mutex::new(None), + reset: Mutex::new(None), + microlan: Mutex::new(None), + node_changed: Mutex::new(None), + } + } + pub fn connect_error(&self, cb: Level) { + let _delivery = self.delivery.lock(); + *self.error.lock() = Some(cb); + *self.driven.lock() = None; + self.update_irq(); + } + /// Attach the node's 1-wire bus to the local-block MicroLAN master. + pub fn set_microlan(&self, bus: w1::Bus) { + *self.microlan.lock() = Some(bus); + } + pub fn connect_soft_reset(&self, cb: Arc) { + *self.reset.lock() = Some(cb); + } + pub fn connect_node_changed(&self, cb: Arc) { + let _delivery = self.delivery.lock(); + *self.node_changed.lock() = Some(cb); + } + pub fn node_number(&self) -> u8 { + self.regs.lock().node + } + pub fn error_level(&self) -> bool { + self.regs.lock().words.get(&0x4040).copied().unwrap_or(0) != 0 + } + fn update_irq(&self) { + // Serialize the sample and callback, while leaving registers available + // to the callback. Suppress unchanged levels so callbacks can reenter + // register reads that recompute the output. + let _delivery = self.delivery.lock(); + let cb = self.error.lock().clone(); + if let Some(cb) = cb { + let level = self.error_level(); + let changed = { + let mut driven = self.driven.lock(); + let changed = *driven != Some(level); + *driven = Some(level); + changed + }; + if changed { + cb(level); + } + } + } + fn notify_node(&self) { + let _delivery = self.delivery.lock(); + let cb = self.node_changed.lock().clone(); + if let Some(cb) = cb { + cb(self.node_number()); + } + } + fn complete(r: &mut Regs, now: u64) { + if r.deadline.is_some_and(|d| now >= d) { + *r.words.entry(0x4030).or_default() |= 2 | u64::from(r.sample); + r.deadline = None; + } + } + fn mask(a: u64) -> Option { + Some(match a { + reg::PIO_PERMISSION | reg::INBOUND_PERMISSION | reg::REGISTER_4020 | reg::REGISTER_4060 | reg::REGISTER_C008 | reg::REGISTER_C010 | reg::REGISTER_C028 | reg::REGISTER_C030 => !0, + reg::PIO_REGION_MASK => 0x80ff80ff80ff80ff, + reg::ERROR_STATUS => 0x7ff, + reg::NODE_REGION_CONFIG => 0x7f | (0x7ff << 8) | (0x7ff << 20) | (0xf << 32) | (1 << 40) | (1 << 44), + reg::NODE_REGION_MASK => (1 << 38) - 1, + reg::PIO_CONFIG => 0x3f, + reg::INBOUND_CONFIG => 1 | (0x3ff << 4) | (0x3ff << 16) | (1 << 28) | (1 << 32), + reg::INBOUND_MASK => { + 0x1f | (0xfff << 8) | (1 << 20) | (0x3ff << 24) | (0x3ff << 36) | (0x3ff << 48) + } + reg::VECTOR_REQUEST => VECTOR_MASK, + _ => return None, + }) + } +} +impl Resettable for BedrockLb { + fn power_on(&self) { + let mut r = self.regs.lock(); + let survivors = [ + r.scratch[1], + r.scratch[2], + r.words.get(®::ERROR_STATUS).copied().unwrap_or(0), + ]; + *r = Regs::default(); + r.scratch[1] = survivors[0]; + r.scratch[2] = survivors[1]; + r.words.insert(reg::ERROR_STATUS, survivors[2]); + drop(r); + self.update_irq(); + self.notify_node(); + } +} +impl BusDevice for BedrockLb { + fn read64(&self, a: u64) -> BusRead64 { + if a & 7 != 0 { + return BusRead64::err(); + } + let now = (self.clock)(); + let mut r = self.regs.lock(); + Self::complete(&mut r, now); + let v = match a { + reg::NODE_REGION => ID | ((r.node as u64) << 32) | ((r.region as u64) << 46), + reg::PIO_WINDOW..=reg::PIO_WINDOW_END => r.scratch[(a as usize - reg::PIO_WINDOW as usize) / 8], + reg::ATOMIC_READ0 | reg::ATOMIC_READ1 => { + let n = 3 + (a as usize - reg::ATOMIC_READ0 as usize) / 8; + let v = r.scratch[n]; + r.scratch[n] = 1; + v + } + reg::VECTOR_STATUS_ALIAS => r.words.remove(&0xc020).unwrap_or(0), + reg::VECTOR_STATUS | reg::MICRO_LAN => r.words.get(&a).copied().unwrap_or(0), + _ if Self::mask(a).is_some() => r.words.get(&a).copied().unwrap_or(0), + _ => { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB unsupported read {a:#x}"); + 0 + } + }; + BusRead64::ok(v) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + let now = (self.clock)(); + let mut r = self.regs.lock(); + Self::complete(&mut r, now); + match a { + reg::NODE_REGION => { + let region = ((v >> 46) & 3) as u8; + if region == 3 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB invalid region size"); + } else { + r.node = (v >> 32) as u8; + r.region = region; + } + } + reg::PIO_WINDOW..=reg::PIO_WINDOW_END => { + let n = (a as usize - 0x8000) / 8; + r.scratch[n] = if n < 3 { v } else { v & 1 }; + } + reg::ATOMIC_SCRATCH..=reg::ATOMIC_SCRATCH_END => { + let n = (a as usize - reg::ATOMIC_SCRATCH as usize) / 8; + if r.scratch[n] == 0 { + r.scratch[n] = v; + } + } + reg::PIO_PERMISSION_KEY => { + if v == IIO_PROTECT_OVRRD_KEY { + *r.words.entry(reg::PIO_PERMISSION).or_default() |= 1; + } + } + reg::SOFT_RESET => {} + reg::MICRO_LAN => { + if r.deadline.is_some() { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB MicroLAN write while slot pending discarded"); + } else { + r.words.insert(a, v & ((1 << 34) - 4)); + let (pulse, sample) = w1::slot::fields(v); + r.sample = match self.microlan.lock().as_mut() { + Some(bus) => w1::slot::slot(bus, pulse, sample), + None => pulse >= w1::slot::RESET_PULSE + || pulse < w1::slot::ONE_PULSE, + }; + r.deadline = Some(now.saturating_add(1000)); + } + } + reg::ERROR_STATUS => { + *r.words.entry(a).or_default() |= v & 0x7ff; + } + reg::ERROR_CLEAR => { + *r.words.entry(0x4040).or_default() &= !(v & 0x7ff); + } + reg::VECTOR_REQUEST => { + r.words.insert(a, v & VECTOR_MASK); + if r.words.get(®::VECTOR_STATUS).copied().unwrap_or(0) & (1 << 63) != 0 { + *r.words.entry(reg::VECTOR_STATUS).or_default() |= 1 << 62; + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB vector launch reaches no peer; no reply latched"); + } + } + reg::VECTOR_STATUS | reg::VECTOR_STATUS_ALIAS => { + if v == 0 { + r.words.insert(reg::VECTOR_STATUS, 0); + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB nonzero vector status write ignored"); + } + } + _ => { + if let Some(mask) = Self::mask(a) { + r.words.insert(a, v & mask); + if (a == reg::PIO_PERMISSION || a == reg::INBOUND_PERMISSION) && v != !0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB permission enforcement requires requester context"); + } + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock LB unsupported write {a:#x} = {v:#x}"); + } + } + } + drop(r); + self.update_irq(); + if a == reg::NODE_REGION { + self.notify_node(); + } + if a == reg::SOFT_RESET && v & 1 != 0 { + self.power_on(); + let cb = self.reset.lock().clone(); + if let Some(cb) = cb { + cb(); + } + } + BUS_OK + } +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sn1-bedrock-lb-test.c in + // jamesbraid/qemu-sgi-origami: identity-permissions, scratch-reset and + // microlan-slot. + use super::*; + use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering}; + /// Hold an IRQ packet's assertion callback while a CPU clears the register. + pub(super) fn check_order( + device: Arc, + install: impl FnOnce(Level), + raise: u64, + value: u64, + clear: impl FnOnce() + Send + 'static, + ) { + let (entered_tx, entered_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = std::sync::mpsc::channel(); + let release_rx = Mutex::new(release_rx); + let delivered = Arc::new(AtomicBool::new(false)); + let sink = delivered.clone(); + install(Arc::new(move |level| { + if level { + entered_tx.send(()).unwrap(); + release_rx + .lock() + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + } + sink.store(level, Ordering::SeqCst); + })); + let io = crate::dev::bedrock_io::BedrockIo::new(); + io.connect_hub(device); + let first = std::thread::spawn(move || { + io.inbound_write(14, Some(0), (1 << 47) | 0x1800000 | raise, 8, value) + }); + entered_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let second = std::thread::spawn(move || { + clear(); + done_tx.send(()).unwrap(); + }); + // An ordered callback waits for the blocked assertion. A stale publisher + // instead completes the clear early, then overwrites it after release. + let _ = done_rx.recv_timeout(std::time::Duration::from_millis(100)); + release_tx.send(()).unwrap(); + assert_eq!(first.join().unwrap(), BUS_OK); + second.join().unwrap(); + assert!(!delivered.load(Ordering::SeqCst)); + } + + /// The guest configures node permissions and expects selected scratch words to survive reset. + #[test] + fn permission_writes_latch_errors_and_reset_retains_scratch() { + { + let d = BedrockLb::new(Arc::new(|| 0)); + d.write64(reg::NODE_REGION, (0x5a << 32) | (2 << 46)); + assert_eq!(d.node_number(), 0x5a); + d.write64(reg::NODE_REGION, 3 << 46); + assert_eq!(d.node_number(), 0x5a); + d.write64(reg::PIO_PERMISSION, 0); + d.write64(reg::PIO_PERMISSION_KEY, 0); + assert_eq!(d.read64(reg::PIO_PERMISSION).data, 0); + d.write64(reg::PIO_PERMISSION_KEY, IIO_PROTECT_OVRRD_KEY); + assert_eq!(d.read64(reg::PIO_PERMISSION).data, 1); + d.write64(reg::PIO_WINDOW + 8, 42); + d.write64(reg::ERROR_STATUS, !0); + assert_eq!(d.read64(reg::ERROR_STATUS).data, 0x7ff); + d.power_on(); + assert_eq!(d.read64(reg::PIO_WINDOW + 8).data, 42); + assert!(d.error_level()); + d.write64(reg::ERROR_CLEAR, 0x7ff); + assert!(!d.error_level()); + assert_eq!(d.node_number(), 0); + } + { + let d = BedrockLb::new(Arc::new(|| 0)); + d.write64(reg::ATOMIC_SCRATCH, 42); + d.write64(reg::ATOMIC_SCRATCH, 43); + assert_eq!(d.read64(reg::PIO_WINDOW).data, 42); + d.write64(reg::REGISTER_C030, 0xdead); + d.write64(reg::VECTOR_REQUEST, !0); + assert_eq!(d.read64(reg::VECTOR_STATUS).data, 0); + assert_eq!(d.read64(reg::REGISTER_C030).data, 0xdead); + assert_eq!(d.read32(reg::PIO_WINDOW).status, BUS_ERR); + assert_eq!(d.read64(1).status, BUS_ERR); + } + } + /// The guest polls MicroLAN completion and cannot replace a slot that is still pending. + #[test] + fn microlan_slots_finish_after_one_microsecond_and_reject_overlap() { + let time = Arc::new(AtomicU64::new(0)); + let t = time.clone(); + let d = BedrockLb::new(Arc::new(move || t.load(Ordering::SeqCst))); + let word = (0x66 << 27) | (60 << 10) | (0x41 << 2) | 3; + d.write64(reg::MICRO_LAN, word); + assert_eq!(d.read64(reg::MICRO_LAN).data, word & !3); + d.write64(reg::MICRO_LAN, 8 << 10); + assert_eq!(d.read64(reg::MICRO_LAN).data, word & !3); + time.store(1000, Ordering::SeqCst); + assert_eq!(d.read64(reg::MICRO_LAN).data, (word & !3) | 2); + d.write64(reg::MICRO_LAN, 8 << 10); + time.store(2000, Ordering::SeqCst); + assert_eq!(d.read64(reg::MICRO_LAN).data, (8 << 10) | 3); + d.write64(reg::MICRO_LAN, 60 << 10); + d.power_on(); + time.store(3000, Ordering::SeqCst); + assert_eq!(d.read64(reg::MICRO_LAN).data, 0); + } + /// Node-change callbacks may reenter LB while guest writes race. + #[test] + fn notifications_preserve_concurrent_register_order() { + { + let ni = Arc::new(crate::dev::bedrock_ni::BedrockNi::new()); + let a = ni.clone(); + let b = ni.clone(); + check_order( + ni, + move |cb| a.connect_error(cb), + crate::dev::bedrock_ni::reg::ERROR_STATUS, + 1 << 24, + move || { + b.read64(crate::dev::bedrock_ni::reg::ERROR_CLEAR); + }, + ); + } + { + let d = Arc::new(BedrockLb::new(Arc::new(|| 0))); + let a = d.clone(); + let b = d.clone(); + check_order( + d, + move |cb| a.connect_error(cb), + reg::ERROR_STATUS, + 1, + move || { + b.write64(reg::ERROR_CLEAR, 1); + }, + ); + } + { + let d = Arc::new(crate::dev::bedrock_xb::BedrockXb::new()); + let a = d.clone(); + let b = d.clone(); + check_order( + d, + move |cb| a.connect_error(cb), + crate::dev::bedrock_xb::reg::QUEUE_BASE, + 1, + move || { + b.write64(crate::dev::bedrock_xb::reg::CLEAR_QUEUE_BASE, 0); + b.write64(crate::dev::bedrock_xb::reg::CLEAR_FIRST_ERROR, 0); + }, + ); + } + { + let d = Arc::new(BedrockLb::new(Arc::new(|| 0))); + let delivered = Arc::new(AtomicU8::new(0)); + let sink = delivered.clone(); + let (entered_tx, entered_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = std::sync::mpsc::channel(); + let release_rx = Mutex::new(release_rx); + d.connect_node_changed(Arc::new(move |node| { + if node == 1 { + entered_tx.send(()).unwrap(); + release_rx + .lock() + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + } + sink.store(node, Ordering::SeqCst); + })); + let io = crate::dev::bedrock_io::BedrockIo::new(); + io.connect_hub(d.clone()); + let first = std::thread::spawn(move || { + io.inbound_write(14, Some(0), (1 << 47) | 0x1800000, 8, 1 << 32) + }); + entered_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let other = d.clone(); + let second = std::thread::spawn(move || { + other.write64(0, 2 << 32); + done_tx.send(()).unwrap(); + }); + let _ = done_rx.recv_timeout(std::time::Duration::from_millis(100)); + release_tx.send(()).unwrap(); + assert_eq!(first.join().unwrap(), BUS_OK); + second.join().unwrap(); + assert_eq!(d.node_number(), 2); + assert_eq!(delivered.load(Ordering::SeqCst), 2); + } + } +} diff --git a/src/dev/bedrock_ni.rs b/src/dev/bedrock_ni.rs new file mode 100644 index 000000000..06f1ee57c --- /dev/null +++ b/src/dev/bedrock_ni.rs @@ -0,0 +1,335 @@ +//! Bedrock NI exposes node routing, sideband and error registers. Fuel runs as +//! one node without a NUMAlink peer, but the guest still reads and programs +//! these local control words during machine setup. +//! +//! # What is approximated here +//! +//! Routing and sideband writes are retained; the network fabric and packet +//! delivery are not simulated. Unspecified power-on fields start at zero, and +//! the sideband word survives reset as the current compatibility behavior. +//! +//! Ported from hw/misc/sgi-bedrock-ni.c and hw/misc/sgi-bedrock-ni-regs.c +//! in jamesbraid/qemu-sgi-origami. + +/// Length of the block-relative register aperture. +pub const SIZE: u64 = 0x2008; + +pub mod reg { + pub const NODE_ID: u64 = 0x0; + pub const LOCAL_RESET: u64 = 0x8; + pub const CONTROL_0: u64 = 0x10; + pub const CONTROL_1: u64 = 0x18; + pub const CONTROL_2: u64 = 0x20; + pub const CONTROL_3: u64 = 0x30; + pub const CONTROL_4: u64 = 0x38; + pub const ERROR_STATUS: u64 = 0x100; + pub const ERROR_STATUS_UPPER: u64 = 0x108; + pub const ERROR_STATUS_LOWER: u64 = 0x110; + pub const SIDE_BAND: u64 = 0x118; + pub const ERROR_CLEAR: u64 = 0x120; + pub const ROUTES: u64 = 0x1000; + pub const ROUTES_END: u64 = 0x1400; + pub const GLOBAL_CONTROL: u64 = 0x2000; +} + +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::{Mutex, ReentrantMutex}; +use std::sync::Arc; +type Level = Arc; +#[derive(Clone)] +struct Regs { + control: [u64; 8], + routes: [u64; 128], + global: u64, + errors: u64, + sideband: u64, +} +impl Default for Regs { + fn default() -> Self { + Self { + control: [0; 8], + routes: [0; 128], + global: 0, + errors: 0, + sideband: 0, + } + } +} +pub struct BedrockNi { + regs: Arc>, + error: Mutex>, + delivery: ReentrantMutex<()>, + driven: Mutex>, + reset: Mutex>>, +} +impl Default for BedrockNi { + fn default() -> Self { + Self::new() + } +} +impl BedrockNi { + pub fn new() -> Self { + let regs = Arc::new(Mutex::new(Regs::default())); + crate::locks::register_mutex("bedrock-ni", ®s); + Self { + regs, + error: Mutex::new(None), + delivery: ReentrantMutex::new(()), + driven: Mutex::new(None), + reset: Mutex::new(None), + } + } + pub fn connect_error(&self, cb: Level) { + let _delivery = self.delivery.lock(); + *self.error.lock() = Some(cb); + *self.driven.lock() = None; + self.update_irq(); + } + pub fn connect_local_reset(&self, cb: Arc) { + *self.reset.lock() = Some(cb); + } + pub fn error_level(&self) -> bool { + self.regs.lock().errors & 0x1fffffff000000 != 0 + } + fn update_irq(&self) { + // Serialize the sample and callback, while leaving registers available + // to the callback. Suppress unchanged levels so callbacks can reenter + // register reads that recompute the output. + let _delivery = self.delivery.lock(); + let cb = self.error.lock().clone(); + if let Some(cb) = cb { + let level = self.error_level(); + let changed = { + let mut driven = self.driven.lock(); + let changed = *driven != Some(level); + *driven = Some(level); + changed + }; + if changed { + cb(level); + } + } + } + fn mask(a: u64) -> u64 { + match a { + reg::CONTROL_0 => 1, + reg::CONTROL_1 => 7, + reg::CONTROL_2 => 0x1ff, + reg::CONTROL_3 => 0x7efffffff, + reg::CONTROL_4 => 0xffff, + _ => 0, + } + } +} +impl Resettable for BedrockNi { + fn power_on(&self) { + let mut r = self.regs.lock(); + let sideband = r.sideband; + *r = Regs::default(); + r.sideband = sideband; + drop(r); + self.update_irq(); + } +} +impl BusDevice for BedrockNi { + fn read64(&self, a: u64) -> BusRead64 { + if a & 7 != 0 { + return BusRead64::err(); + } + let mut r = self.regs.lock(); + let v = if Self::mask(a) != 0 { + r.control[a as usize / 8] + } else if (reg::ROUTES..reg::ROUTES_END).contains(&a) { + r.routes[(a as usize - 0x1000) / 8] + } else { + match a { + reg::NODE_ID => 0, + reg::ERROR_STATUS => r.errors, + reg::ERROR_STATUS_UPPER | reg::ERROR_STATUS_LOWER => 0, + reg::SIDE_BAND => r.sideband, + reg::ERROR_CLEAR => { + let v = r.errors; + r.errors = 0; + v + } + reg::GLOBAL_CONTROL => r.global, + _ => { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock NI unsupported read {a:#x}"); + 0 + } + } + }; + drop(r); + self.update_irq(); + BusRead64::ok(v) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + let mut r = self.regs.lock(); + if Self::mask(a) != 0 { + r.control[a as usize / 8] = v & Self::mask(a); + } else if (reg::ROUTES..reg::ROUTES_END).contains(&a) { + r.routes[(a as usize - 0x1000) / 8] = v & 31; + } else { + match a { + reg::LOCAL_RESET => {} + reg::ERROR_STATUS => r.errors = v & ((1 << 53) - 1), + reg::SIDE_BAND => r.sideband = v & 0xfffff, + reg::GLOBAL_CONTROL => r.global = v & 31, + _ => crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock NI unsupported or read-only write {a:#x} = {v:#x}"), + } + } + drop(r); + self.update_irq(); + if a == reg::LOCAL_RESET && v & 4 != 0 { + let cb = self.reset.lock().clone(); + if let Some(cb) = cb { + cb(); + } + } + BUS_OK + } +} + +#[cfg(test)] +mod tests { + // Cases ported from tests/qtest/sn1-bedrock-test.c in jamesbraid/qemu-sgi-origami. + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + /// The guest programs route fields and reads the error register to acknowledge faults. + #[test] + fn route_registers_store_fields_and_error_reads_clear_pending_errors() { + let d = Arc::new(BedrockNi::new()); + for (a, mask) in [ + (reg::CONTROL_0, 1), + (reg::CONTROL_1, 7), + (reg::CONTROL_2, 0x1ff), + (reg::CONTROL_3, 0x7efffffff), + (reg::CONTROL_4, 0xffff), + ] { + d.write64(a, !0); + assert_eq!(d.read64(a).data, mask); + } + for a in [reg::ROUTES, reg::ROUTES_END - 8] { + d.write64(a, !0); + assert_eq!(d.read64(a).data, 31); + } + d.write64(reg::GLOBAL_CONTROL, !0); + assert_eq!(d.read64(reg::GLOBAL_CONTROL).data, 31); + let weak = Arc::downgrade(&d); + d.connect_error(Arc::new(move |_| { + let d = weak.upgrade().unwrap(); + let _ = d.read64(reg::SIDE_BAND); + })); + let resets = Arc::new(AtomicU64::new(0)); + let count = resets.clone(); + d.connect_local_reset(Arc::new(move || { + count.fetch_add(1, Ordering::SeqCst); + })); + d.write64(reg::CONTROL_0, 0); + d.write64(reg::LOCAL_RESET, 4); + assert_eq!(resets.load(Ordering::SeqCst), 1); + d.write64(reg::ERROR_STATUS, 0xffffff); + assert!(!d.error_level()); + d.write64(reg::ERROR_STATUS, 1 << 24); + assert!(d.error_level()); + d.read64(reg::ERROR_CLEAR); + assert!(!d.error_level()); + d.write64(reg::SIDE_BAND, !0); + d.power_on(); + assert_eq!(d.read64(reg::SIDE_BAND).data, 0xfffff); + assert_eq!(d.read64(reg::CONTROL_3).data, 0); + } + + /// The PROM's one power-on pass writes a sign-extended port-parameter constant and zero + /// to the error and sideband words; each must read back through its defined bits only. + #[test] + fn power_on_programming_pass_reads_back_through_the_defined_bits() { + let d = BedrockNi::new(); + d.write64(reg::CONTROL_0, 1); + d.write64(reg::CONTROL_1, 0); + d.write64(reg::CONTROL_2, 0x166); + d.write64(reg::CONTROL_3, 0xffff_ffff_a3ff_1990); + d.write64(reg::CONTROL_4, 0xc800); + d.write64(reg::ERROR_STATUS, 0); + d.read64(reg::ERROR_CLEAR); + d.write64(reg::SIDE_BAND, 0); + for (a, want) in [ + (reg::CONTROL_0, 1), + (reg::CONTROL_1, 0), + (reg::CONTROL_2, 0x166), + (reg::CONTROL_3, 0x7_a3ff_1990), + (reg::CONTROL_4, 0xc800), + (reg::ERROR_STATUS, 0), + (reg::SIDE_BAND, 0), + (reg::ERROR_CLEAR, 0), + ] { + assert_eq!(d.read64(a).data, want, "register {a:#x}"); + } + } + + /// Only the local-reset bit of the reset word reaches the node; the other bits must + /// not restart it, and the interface's own registers are left to the board's reset. + #[test] + fn only_the_local_reset_bit_requests_a_node_reset() { + let d = BedrockNi::new(); + let resets = Arc::new(AtomicU64::new(0)); + let count = resets.clone(); + d.connect_local_reset(Arc::new(move || { + count.fetch_add(1, Ordering::SeqCst); + })); + let mut expected = 0; + for enable in 0..2 { + d.write64(reg::CONTROL_0, enable); + for command in 0..8u64 { + d.write64(reg::LOCAL_RESET, command); + expected += u64::from(command & 4 != 0); + assert_eq!(resets.load(Ordering::SeqCst), expected, "command {command:#x}"); + } + } + // With nothing wired to the callback the request does not clear this block. + assert_eq!(d.read64(reg::CONTROL_0).data, 1); + } + + /// A latched port error raises the interrupt line through its interrupting bits only; + /// drivers rely on the line dropping when the status is rewritten or cleared. + #[test] + fn stored_error_status_drives_the_error_line_through_the_interrupting_bits() { + let d = BedrockNi::new(); + let level = Arc::new(AtomicU64::new(0)); + let seen = level.clone(); + d.connect_error(Arc::new(move |high| seen.store(u64::from(high), Ordering::SeqCst))); + for (value, high) in [(1u64 << 30, 1), (0xff, 0), (1 << 52, 1), (0, 0)] { + d.write64(reg::ERROR_STATUS, value); + assert_eq!(level.load(Ordering::SeqCst), high, "status {value:#x}"); + } + // Bit 53 is beyond the register and must not be retained. + d.write64(reg::ERROR_STATUS, 1 << 53); + assert_eq!(d.read64(reg::ERROR_STATUS).data, 0); + } + + /// Reset values of the programmable words are unknown on hardware, so the model clears them; + /// a guest that re-runs its setup after a reset must see zeroes, not stale routes. + #[test] + fn reset_clears_every_programmable_register_and_misaligned_accesses_fault() { + let d = BedrockNi::new(); + let offsets = [ + reg::CONTROL_0, reg::CONTROL_1, reg::CONTROL_2, reg::CONTROL_3, reg::CONTROL_4, + reg::ROUTES, reg::ROUTES_END - 8, reg::GLOBAL_CONTROL, + ]; + for a in offsets { + d.write64(a, !0); + } + // A misaligned write must not land in the neighbouring register. + assert_eq!(d.write64(reg::CONTROL_4 + 1, 0), BUS_ERR); + assert_eq!(d.read64(reg::CONTROL_4 + 1).status, BUS_ERR); + assert_eq!(d.read64(reg::CONTROL_4).data, 0xffff); + d.power_on(); + for a in offsets { + assert_eq!(d.read64(a).data, 0, "register {a:#x}"); + } + } +} diff --git a/src/dev/bedrock_xb.rs b/src/dev/bedrock_xb.rs new file mode 100644 index 000000000..6df8e7b1c --- /dev/null +++ b/src/dev/bedrock_xb.rs @@ -0,0 +1,278 @@ +//! Bedrock XB is the node's internal crossbar status block. It records the +//! fabric error queues and lets software service queued errors; the Fuel board +//! connects its outputs to the Bedrock processor interface. +//! +//! # What is approximated here +//! +//! Arbitration timing and diagnostic outputs are not modeled. Unsupported +//! queue reads return zero, while writes are logged and ignored. +//! +//! Ported from hw/misc/sgi-bedrock-xb.c in jamesbraid/qemu-sgi-origami. + +/// Length of the block-relative register aperture. +pub const SIZE: u64 = 0x198; + +pub mod reg { + pub const CONTROL_BASE: u64 = 0x0; + pub const CONTROL_END: u64 = 0x18; + pub const QUEUE_BASE: u64 = 0x20; + pub const QUEUE_END: u64 = 0x88; + pub const FIRST_ERROR: u64 = 0x90; + pub const DEBUG_BASE: u64 = 0x98; + pub const DEBUG_END: u64 = 0xa0; + pub const CLEAR_QUEUE_BASE: u64 = 0x120; + pub const CLEAR_QUEUE_END: u64 = 0x188; + pub const CLEAR_FIRST_ERROR: u64 = 0x190; +} + +use crate::traits::{BusDevice, BusRead64, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::{Mutex, ReentrantMutex}; +use std::sync::Arc; +type Level = Arc; +const MASKS: [u64; 14] = [ + 0x3333, 0x33, 0, 0, 0x3330, 0, 0, 0xf0, 0xfff0, 0, 0, 0, 0x3303, 0x11, +]; +struct Regs { + control: [u64; 4], + queues: [u64; 14], + first: u64, + debug: [u64; 2], + unserviced: u16, +} +impl Default for Regs { + fn default() -> Self { + Self { + control: [0x0008000080000021, 0, 0, 0], + queues: [0; 14], + first: 0, + debug: [0; 2], + unserviced: 0, + } + } +} +pub struct BedrockXb { + regs: Arc>, + error: Mutex>, + delivery: ReentrantMutex<()>, + driven: Mutex>, +} +impl Default for BedrockXb { + fn default() -> Self { + Self::new() + } +} +impl BedrockXb { + pub fn new() -> Self { + let regs = Arc::new(Mutex::new(Regs::default())); + crate::locks::register_mutex("bedrock-xb", ®s); + Self { + regs, + error: Mutex::new(None), + delivery: ReentrantMutex::new(()), + driven: Mutex::new(None), + } + } + pub fn connect_error(&self, cb: Level) { + let _delivery = self.delivery.lock(); + *self.error.lock() = Some(cb); + *self.driven.lock() = None; + self.update_irq(); + } + pub fn error_level(&self) -> bool { + self.regs.lock().first & 0x1000 != 0 + } + fn update_irq(&self) { + // Serialize the sample and callback, while leaving registers available + // to the callback. Suppress unchanged levels so callbacks can reenter + // register reads that recompute the output. + let _delivery = self.delivery.lock(); + let cb = self.error.lock().clone(); + if let Some(cb) = cb { + let level = self.error_level(); + let changed = { + let mut driven = self.driven.lock(); + let changed = *driven != Some(level); + *driven = Some(level); + changed + }; + if changed { + cb(level); + } + } + } +} +impl Resettable for BedrockXb { + fn power_on(&self) { + *self.regs.lock() = Regs::default(); + self.update_irq(); + } +} +impl BusDevice for BedrockXb { + fn read64(&self, a: u64) -> BusRead64 { + if a & 7 != 0 { + return BusRead64::err(); + } + let r = self.regs.lock(); + let v = match a { + reg::CONTROL_BASE..=reg::CONTROL_END => r.control[a as usize / 8], + reg::QUEUE_BASE..=reg::QUEUE_END => { + let n = (a as usize - 0x20) / 8; + if MASKS[n] == 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB unsupported queue {n}"); + } + r.queues[n] + } + reg::FIRST_ERROR => r.first, + reg::DEBUG_BASE | reg::DEBUG_END => r.debug[(a as usize - 0x98) / 8], + _ => { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB unsupported read {a:#x}"); + 0 + } + }; + BusRead64::ok(v) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + let mut r = self.regs.lock(); + match a { + reg::CONTROL_BASE..=reg::CONTROL_END => { + let n = a as usize / 8; + r.control[n] = v & [0x000ffffffffffff1, 0xffffffff, 0x7ff, 0xf][n]; + if a == 0x10 && r.control[n] != 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB traffic observation unsupported"); + } + } + reg::QUEUE_BASE..=reg::QUEUE_END => { + let n = (a as usize - 0x20) / 8; + let v = v & MASKS[n]; + if MASKS[n] == 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB unsupported queue {n}"); + } + if v != 0 { + r.queues[n] |= v; + r.unserviced |= 1 << n; + if r.first & 0x1000 == 0 { + r.first = 0x1000 | ((15 - n) as u64) << 8; + } + } + } + reg::CLEAR_QUEUE_BASE..=reg::CLEAR_QUEUE_END => { + let n = (a as usize - 0x120) / 8; + r.queues[n] = 0; + r.unserviced &= !(1 << n); + } + reg::CLEAR_FIRST_ERROR => { + if r.first & 0x1000 != 0 { + let n = 15 - ((r.first >> 8) & 15) as usize; + if r.unserviced & (1 << n) != 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB first error cleared before queue"); + } else { + r.first = 0; + for n in 0..14 { + if r.queues[n] != 0 { + r.first = 0x1000 | ((15 - n) as u64) << 8; + break; + } + } + } + } + } + reg::DEBUG_BASE | reg::DEBUG_END => { + let n = (a as usize - 0x98) / 8; + r.debug[n] = v & [0xffff, 7][n]; + if r.debug[n] != 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB debug output unsupported"); + } + } + _ => crate::dlog!(crate::devlog::LogModule::Bedrock, "Bedrock XB unsupported or read-only write {a:#x} = {v:#x}"), + } + drop(r); + self.update_irq(); + BUS_OK + } +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sn1-bedrock-xb-test.c in + // jamesbraid/qemu-sgi-origami: control-storage, service-order and + // cold-reset. + use super::*; + + /// The guest sees masked crossbar control words and cleared errors after reset. + #[test] + fn crossbar_control_masks_apply_and_reset_clears_errors() { + let d = BedrockXb::new(); + for (a, mask) in [ + (reg::CONTROL_BASE, 0x000ffffffffffff1), + (reg::CONTROL_BASE + 8, 0xffffffff), + (reg::CONTROL_BASE + 0x10, 0x7ff), + (reg::CONTROL_END, 15), + (reg::DEBUG_BASE, 0xffff), + (reg::DEBUG_END, 7), + ] { + d.write64(a, !0); + assert_eq!(d.read64(a).data, mask); + } + // Keep expected register values independent of the decoder's mask table. + for (address, expected) in [ + (reg::QUEUE_BASE, 0x3333), + (reg::QUEUE_BASE + 8, 0x0033), + (reg::QUEUE_BASE + 0x10, 0), + (reg::QUEUE_BASE + 0x18, 0), + (reg::QUEUE_BASE + 0x20, 0x3330), + (reg::QUEUE_BASE + 0x28, 0), + (reg::QUEUE_BASE + 0x30, 0), + (reg::QUEUE_BASE + 0x38, 0x00f0), + (reg::QUEUE_BASE + 0x40, 0xfff0), + (reg::QUEUE_BASE + 0x48, 0), + (reg::QUEUE_BASE + 0x50, 0), + (reg::QUEUE_BASE + 0x58, 0), + (reg::QUEUE_BASE + 0x60, 0x3303), + (reg::QUEUE_END, 0x0011), + ] { + d.write64(address, !0); + assert_eq!(d.read64(address).data, expected, "register {address:#x}"); + } + d.power_on(); + assert_eq!(d.read64(reg::CONTROL_BASE).data, 0x0008000080000021); + assert!(!d.error_level()); + assert_eq!(d.read32(reg::CONTROL_BASE).status, BUS_ERR); + } + /// The guest services queued crossbar faults in order, one error at a time. + #[test] + fn servicing_one_crossbar_error_exposes_the_next_pending_error() { + { + let d = BedrockXb::new(); + d.write64(reg::QUEUE_BASE, 1); + assert!(d.error_level()); + assert_eq!(d.read64(reg::FIRST_ERROR).data, 0x1f00); + d.write64(reg::CLEAR_FIRST_ERROR, !0); + assert_eq!(d.read64(reg::FIRST_ERROR).data, 0x1f00); + d.write64(reg::CLEAR_QUEUE_END, !0); + d.write64(reg::CLEAR_QUEUE_BASE, 0); + d.write64(reg::CLEAR_FIRST_ERROR, 0); + assert!(!d.error_level()); + d.power_on(); + assert!(!d.error_level()); + } + { + let d = BedrockXb::new(); + d.write64(reg::QUEUE_END, 0x10); + d.write64(reg::QUEUE_BASE, 1); + assert_eq!(d.read64(reg::FIRST_ERROR).data, 0x1200); + d.write64(reg::CLEAR_QUEUE_END, 0); + d.write64(reg::CLEAR_FIRST_ERROR, 0); + assert_eq!(d.read64(reg::FIRST_ERROR).data, 0x1f00); + d.write64(reg::CLEAR_QUEUE_BASE, 0); + d.write64(reg::QUEUE_BASE, 0x10); + d.write64(reg::CLEAR_FIRST_ERROR, 0); + assert_eq!(d.read64(reg::FIRST_ERROR).data, 0x1f00); + d.write64(reg::CLEAR_QUEUE_BASE, 0); + d.write64(reg::CLEAR_FIRST_ERROR, 0); + assert!(!d.error_level()); + } + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 013e5b626..8e5552271 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,6 +60,10 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod bedrock_io; +pub mod bedrock_ni; +pub mod bedrock_xb; +pub mod bedrock_lb; pub mod bedrock_md; pub mod bedrock_mem; pub mod dp83843; From 567efb1c2805252f91670510a35be48986131108 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 11/20] bedrock_pi: add processor control and interrupts The Fuel CPU receives interrupts and timing events through Bedrock PI. Model the fitted processor slot and its node callbacks while keeping unconnected graphics control explicit. --- src/dev/bedrock_pi.rs | 757 ++++++++++++++++++++++++++++++++++++ src/dev/bedrock_pi/tests.rs | 276 +++++++++++++ src/dev/mod.rs | 1 + 3 files changed, 1034 insertions(+) create mode 100644 src/dev/bedrock_pi.rs create mode 100644 src/dev/bedrock_pi/tests.rs diff --git a/src/dev/bedrock_pi.rs b/src/dev/bedrock_pi.rs new file mode 100644 index 000000000..7e44a44c4 --- /dev/null +++ b/src/dev/bedrock_pi.rs @@ -0,0 +1,757 @@ +//! Bedrock PI exposes processor-slot control, pending interrupts, real-time +//! counters, timers and shared graphics registers. Fuel uses PI0 slot A for +//! its processor; the other processor slot has no device attached. Node hooks +//! carry cache-alias changes, CPU enable and NMI events to the board. +//! +//! # What is approximated here +//! +//! The model accepts Linux's public `SGIrules` key at offset 0x8 for +//! compatibility. Linux documents the literal for Hub I/O, but the available +//! evidence does not establish that this PI offset maps to the same register. +//! Unconnected graphics registers read as zero and are logged as unsupported. +//! +//! Ported from hw/misc/sgi-bedrock-pi.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::bedrock_io::IIO_PROTECT_OVRRD_KEY; +use crate::dev::heart::Clock; +use crate::hptimer::{TimerId, TimerManager, TimerReturn}; +use crate::irq::InterruptController; +use crate::traits::{BusDevice, BusRead32, BusRead64, Device, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::{Mutex, ReentrantMutex}; +use std::sync::{Arc, OnceLock, Weak}; +use std::time::Duration; +const RT_MASK: u64 = (1 << 55) - 1; +const ERROR_MASK: u64 = (1 << 35) - 1; +const MEMORY_ERROR: u64 = 1 << 24; +const REFLECT: u64 = (1 << 54) | (1 << 55) | (1 << 62) | (1 << 63); +const TICK: u64 = 800; +pub const SIZE: u64 = 0x30108; + +pub mod reg { + pub const NODE_REGION: u64 = 0x0; + pub const PROTECTION_KEY: u64 = 0x8; + pub const OFFSET_0010: u64 = 0x10; + pub const OFFSET_0018: u64 = 0x18; + pub const INTERFACE_ID: u64 = 0x20; + pub const CACHED_ALIAS: u64 = 0x28; + pub const OFFSET_0030: u64 = 0x30; + pub const OFFSET_0038: u64 = 0x38; + pub const INTERFACE_SLOT_A: u64 = 0x40; + pub const INTERFACE_SLOT_B: u64 = 0x48; + pub const CPU_A_ENABLE: u64 = 0x50; + pub const CPU_B_ENABLE: u64 = 0x58; + pub const NMI_SLOT_A: u64 = 0x70; + pub const NMI_SLOT_B: u64 = 0x78; + pub const PENDING_MODIFY: u64 = 0x90; + pub const PENDING_STATUS_A: u64 = 0x98; + pub const PENDING_STATUS_B: u64 = 0xa0; + pub const OFFSET_00A8: u64 = 0xa8; + pub const OFFSET_00B0: u64 = 0xb0; + pub const OFFSET_00B8: u64 = 0xb8; + pub const OFFSET_00C0: u64 = 0xc0; + pub const REQUEST_A: u64 = 0xc8; + pub const REQUEST_B: u64 = 0xd0; + pub const PENDING_A_CLEAR: u64 = 0xd8; + pub const PENDING_B_CLEAR: u64 = 0xe0; + pub const OFFSET_00E8: u64 = 0xe8; + pub const OFFSET_00F0: u64 = 0xf0; + pub const RT_COMPARE_A: u64 = 0x108; + pub const RT_COMPARE_B: u64 = 0x110; + pub const PROFILE_COMPARE: u64 = 0x118; + pub const RT_STATUS_A: u64 = 0x120; + pub const RT_STATUS_B: u64 = 0x128; + pub const PROFILE_STATUS_A: u64 = 0x130; + pub const PROFILE_STATUS_B: u64 = 0x138; + pub const RT_ENABLE_A: u64 = 0x140; + pub const RT_ENABLE_B: u64 = 0x148; + pub const PROFILE_ENABLE_A: u64 = 0x150; + pub const PROFILE_ENABLE_B: u64 = 0x158; + pub const PENDING_MODIFY_B: u64 = 0x180; + pub const ERROR_SUMMARY_ALIAS: u64 = 0x3f8; + pub const ERROR_SUMMARY: u64 = 0x400; + pub const ERROR_CAUSE_A: u64 = 0x408; + pub const ERROR_CAUSE_B: u64 = 0x410; + pub const ERROR_ADDRESS_A: u64 = 0x418; + pub const ERROR_ADDRESS_B: u64 = 0x420; + pub const ERROR_CONTROL: u64 = 0x428; + pub const ERROR_CAPTURE_A: u64 = 0x438; + pub const ERROR_CAPTURE_B: u64 = 0x448; + pub const ERROR_CAPTURE_C: u64 = 0x458; + pub const ERROR_CAPTURE_D: u64 = 0x468; + pub const ERROR_SOURCE_A: u64 = 0x490; + pub const ERROR_SOURCE_B: u64 = 0x4c0; + pub const ERROR_DETAIL_A: u64 = 0x4a8; + pub const ERROR_DETAIL_B: u64 = 0x4b0; + pub const ERROR_DETAIL_C: u64 = 0x4b8; + pub const OFFSET_0430: u64 = 0x430; + pub const OFFSET_0440: u64 = 0x440; + pub const OFFSET_0450: u64 = 0x450; + pub const OFFSET_0460: u64 = 0x460; + pub const OFFSET_04C8: u64 = 0x4c8; + pub const OFFSET_04D0: u64 = 0x4d0; + pub const CREDIT_MODE: u64 = 0x20068; + pub const COUNTER: u64 = 0x30100; + pub const GRAPHICS_BASE: u64 = 0x300; + pub const GRAPHICS_END: u64 = 0x350; +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct BedrockPiConfig { + pub interface: u8, + pub request_region: u8, +} +/// Called outside the state lock. Graphics indices 0..9 are the shared PI +/// graphics registers at offsets 0x300..0x348. Disconnected access is observable. +pub trait NodeHooks: Send + Sync { + fn cached_alias_changed(&self, _size: u8) {} + fn rt_compare_written(&self, _interface: u8, _slot: usize) {} + fn rt_counter_read(&self, _interface: u8) {} + fn cpu_enable(&self, _interface: u8, _slot: usize, _enabled: bool) {} + fn nmi(&self, _interface: u8, _slot: usize) {} + fn gfx_read(&self, _index: usize) -> Option { + None + } + fn gfx_write(&self, _index: usize, _value: u64) -> bool { + false + } + fn gfx_reset(&self) {} +} +struct State { + w: [u64; 160], + pending: [u64; 2], + reflected: u64, + uart: bool, + base: u64, + epoch: u64, + paused: Option, + running: bool, + rt_due: [u128; 2], + profile_due: u128, + levels: [u8; 256], + rearm: [u64; 128], + credit_mode: u64, +} +impl State { + fn new(now: u64) -> Self { + let mut s = Self { + w: [0; 160], + pending: [0; 2], + reflected: 0, + uart: false, + base: 0, + epoch: now, + paused: Some(now), + running: false, + rt_due: [0; 2], + profile_due: 0, + levels: [0; 256], + rearm: [0; 128], + credit_mode: 0, + }; + s.w[0] = !0; + s.w[2] = !0; + s.w[3] = !0; + s.arm_all(now); + s + } + fn time(&self, now: u64) -> u64 { + self.paused.unwrap_or(now) + } + fn count(&self, now: u64) -> u64 { + self.base + .wrapping_add(self.time(now).saturating_sub(self.epoch) / TICK) + & RT_MASK + } + fn arm_rt(&mut self, slot: usize, now: u64) { + let now = self.time(now); + let ticks = (self.w[0x108 / 8 + slot] + .wrapping_sub(self.count(now)) + .wrapping_sub(1) + & RT_MASK) + + 1; + self.rt_due[slot] = u128::from(now - now.saturating_sub(self.epoch) % TICK) + + u128::from(ticks) * u128::from(TICK); + } + fn arm_profile(&mut self, now: u64) { + let now = self.time(now); + let ticks = (self.w[0x118 / 8] as u32).wrapping_sub(self.count(now) as u32); + let ticks = if ticks == 0 { + 1u64 << 32 + } else { + u64::from(ticks) + }; + self.profile_due = u128::from(now - now.saturating_sub(self.epoch) % TICK) + + u128::from(ticks) * u128::from(TICK); + } + fn arm_all(&mut self, now: u64) { + self.arm_rt(0, now); + self.arm_rt(1, now); + self.arm_profile(now); + } + fn sync(&mut self, now: u64) { + let now = self.time(now); + for slot in 0..2 { + if u128::from(now) >= self.rt_due[slot] { + self.w[0x120 / 8 + slot] = 1; + self.arm_rt(slot, now); + } + } + if u128::from(now) >= self.profile_due { + self.w[0x130 / 8] = 1; + self.w[0x138 / 8] = 1; + self.arm_profile(now); + } + for level in 7..128 { + if self.rearm[level] != 0 && self.rearm[level] <= now { + self.rearm[level] = 0; + if self.levels.iter().any(|&v| usize::from(v) == level + 1) { + self.pending[level / 64] |= 1 << (level % 64); + } + } + } + } + fn pending(&self, bank: usize) -> u64 { + if bank == 1 { + (self.pending[1] & !REFLECT) | self.reflected + } else { + self.pending[0] + | if self.uart { 1 << 4 } else { 0 } + | if self.w[0xc8 / 8] & self.w[0xe8 / 8] != 0 { + 1 << 5 + } else { + 0 + } + | if self.w[0xd0 / 8] & self.w[0xe8 / 8] != 0 { + 1 << 6 + } else { + 0 + } + } + } + fn outputs(&self) -> [bool; 10] { + let mut out = [false; 10]; + for slot in 0..2 { + out[slot * 5] = self.pending(0) & self.w[0xa8 / 8 + slot * 2] != 0; + out[slot * 5 + 1] = self.pending(1) & self.w[0xb0 / 8 + slot * 2] != 0; + out[slot * 5 + 2] = self.w[0x120 / 8 + slot] & self.w[0x140 / 8 + slot] != 0; + out[slot * 5 + 3] = self.w[0x130 / 8 + slot] & self.w[0x150 / 8 + slot] != 0; + out[slot * 5 + 4] = self.w[0x400 / 8] & self.w[0x408 / 8 + slot] != 0; + } + out + } + fn modify(&mut self, value: u64, reissue: bool, now: u64) { + let bank = ((value >> 6) & 1) as usize; + let bit = value & 63; + let mask = 1 << bit; + if bank == 1 && mask & REFLECT != 0 { + return; + } + if value & 0x100 != 0 { + self.pending[bank] |= mask; + } else { + self.pending[bank] &= !mask; + let level = bank * 64 + bit as usize; + if reissue && self.levels.iter().any(|&v| usize::from(v) == level + 1) { + self.rearm[level] = now.saturating_add(1_000_000); + } + } + } +} +pub struct BedrockPi { + cfg: BedrockPiConfig, + clock: Clock, + state: Mutex, + hooks: Mutex>>, + irq: Mutex>>, + timers: OnceLock>, + timer: Mutex>, + timer_pass: Mutex<()>, + dispatch: ReentrantMutex<()>, + driven: Mutex<[Option; 10]>, + weak: Weak, +} +impl BedrockPi { + pub fn new(cfg: BedrockPiConfig, clock: Clock) -> Arc { + assert!(cfg.interface < 2 && cfg.request_region < 64); + Arc::new_cyclic(|weak| Self { + cfg, + state: Mutex::new(State::new(clock())), + clock, + hooks: Mutex::new(None), + irq: Mutex::new(None), + timers: OnceLock::new(), + timer: Mutex::new(None), + timer_pass: Mutex::new(()), + dispatch: ReentrantMutex::new(()), + driven: Mutex::new([None; 10]), + weak: weak.clone(), + }) + } + pub fn set_node_hooks(&self, hooks: Arc) { + let _dispatch = self.dispatch.lock(); + *self.hooks.lock() = Some(hooks); + } + pub fn set_timer_manager(&self, tm: Arc) { + let _ = self.timers.set(tm); + self.schedule(); + } + /// Lines 0..4 are slot A IP2..IP6; lines 5..9 are slot B IP2..IP6. + pub fn set_interrupt_controller(&self, irq: Arc) { + let _dispatch = self.dispatch.lock(); + *self.irq.lock() = Some(irq); + *self.driven.lock() = [None; 10]; + self.drive(); + } + fn hooks(&self) -> Option> { + self.hooks.lock().clone() + } + /// Report an access to a register the model does not implement. + fn note(&self, off: u64, write: bool) { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "Bedrock PI{} unsupported {} at {off:#x}", self.cfg.interface, + if write { "write" } else { "read" }); + } + fn drive(&self) { + // Serialize publication, including reentrant reads by a controller. + // Publish one line at a time: a callback may change another line. + let _dispatch = self.dispatch.lock(); + let irq = self.irq.lock().clone(); + if let Some(irq) = irq { + for line in 0..10 { + let level = self.state.lock().outputs()[line]; + let changed = { + let mut driven = self.driven.lock(); + let changed = driven[line] != Some(level); + driven[line] = Some(level); + changed + }; + if changed { + irq.set_line(line as u32, level); + } + } + } + } + fn schedule(&self) { + let _pass = self.timer_pass.lock(); + let Some(tm) = self.timers.get() else { return }; + let mut timer = self.timer.lock(); + if let Some(id) = timer.take() { + tm.remove(id); + } + let s = self.state.lock(); + if !s.running { + return; + } + let next = s + .rt_due + .iter() + .copied() + .chain([s.profile_due]) + .chain(s.rearm.iter().filter(|&&v| v != 0).map(|&v| u128::from(v))) + .min() + .unwrap(); + let delay = next + .saturating_sub(u128::from((self.clock)())) + .min(1_000_000_000) + .max(1) as u64; + drop(s); + *timer = Some( + tm.add_one_shot(Duration::from_nanos(delay), self.weak.clone(), |weak| { + if let Some(pi) = weak.upgrade() { + { + let _pass = pi.timer_pass.lock(); + let mut s = pi.state.lock(); + if !s.running { + return TimerReturn::Delete; + } + s.sync((pi.clock)()); + } + pi.drive(); + pi.schedule(); + } + TimerReturn::Delete + }), + ); + } + pub fn poll(&self) { + self.state.lock().sync((self.clock)()); + self.drive(); + } + pub fn set_pending(&self, bank: usize, bit: u8) { + if bank < 2 && bit < 64 { + self.state.lock().pending[bank] |= 1 << bit; + self.drive(); + } + } + fn reflected(&self, bit: u8, level: bool) { + let mut s = self.state.lock(); + if level { + s.reflected |= 1 << bit; + } else { + s.reflected &= !(1 << bit); + } + drop(s); + self.drive(); + } + pub fn set_lb_error(&self, level: bool) { + self.reflected(55, level); + } + pub fn set_ni_error(&self, level: bool) { + self.reflected(62, level); + } + pub fn set_xb_error(&self, level: bool) { + self.reflected(54, level); + } + pub fn set_uart_irq(&self, level: bool) { + self.state.lock().uart = level; + self.drive(); + } + pub fn set_memory_uncorrectable(&self) { + self.state.lock().w[0x400 / 8] |= MEMORY_ERROR; + self.drive(); + } + pub fn set_level_source(&self, source: usize, level: u8, active: bool) { + if source < 256 && (!active || (7..=127).contains(&level)) { + self.state.lock().levels[source] = if active { level + 1 } else { 0 }; + self.drive(); + } + } + pub fn io_modify(&self, value: u64, region: u8) -> bool { + let mut s = self.state.lock(); + if region >= 64 || s.w[2] & (1 << region) == 0 { + drop(s); + self.note(0x90, true); + return false; + } + let now = s.time((self.clock)()); + s.modify(value, false, now); + drop(s); + self.drive(); + true + } + fn read(&self, off: u64) -> u64 { + if (reg::GRAPHICS_BASE..reg::GRAPHICS_END).contains(&off) { + if let Some(v) = self + .hooks() + .and_then(|h| h.gfx_read(((off - 0x300) / 8) as usize)) + { + return v; + } + self.note(off, false); + return 0; + } + let mut s = self.state.lock(); + s.sync((self.clock)()); + let value = match off { + reg::INTERFACE_ID => u64::from(self.cfg.interface) << 1, + reg::INTERFACE_SLOT_A => u64::from(self.cfg.interface == 0), + reg::INTERFACE_SLOT_B => 0, + reg::PENDING_STATUS_A => s.pending(0), + reg::PENDING_STATUS_B => s.pending(1), + reg::PENDING_A_CLEAR => s.w[reg::REQUEST_A as usize / 8], + reg::PENDING_B_CLEAR => s.w[reg::REQUEST_B as usize / 8], + reg::ERROR_SUMMARY_ALIAS => s.w[reg::ERROR_SUMMARY as usize / 8], + reg::ERROR_CAPTURE_A | reg::ERROR_CAPTURE_B | reg::ERROR_CAPTURE_C | reg::ERROR_CAPTURE_D => s.w[(off - reg::PROTECTION_KEY) as usize / 8], + reg::CREDIT_MODE => s.credit_mode, + reg::COUNTER => s.count((self.clock)()), + _ if readable(off) => s.w[off as usize / 8], + _ => { + self.note(off, false); + 0 + } + }; + drop(s); + self.drive(); + if off == reg::COUNTER { + if let Some(h) = self.hooks() { + h.rt_counter_read(self.cfg.interface); + } + } + value + } + fn write(&self, off: u64, value: u64) { + // Keep register changes and their node notifications in one order. + // The state lock still drops before callbacks; dispatch is reentrant + // so a node callback can read registers and recompute IRQ outputs. + let _dispatch = self.dispatch.lock(); + if (reg::GRAPHICS_BASE..reg::GRAPHICS_END).contains(&off) { + if !self + .hooks() + .is_some_and(|h| h.gfx_write(((off - 0x300) / 8) as usize, value)) + { + self.note(off, true); + } + return; + } + let now = (self.clock)(); + let mut s = self.state.lock(); + s.sync(now); + let mut alias = None; + let mut compare = None; + let mut enable = None; + let mut nmi = None; + match off { + reg::NODE_REGION | reg::OFFSET_0010 | reg::OFFSET_0018 | reg::OFFSET_00A8 | reg::OFFSET_00B0 | reg::OFFSET_00B8 | reg::OFFSET_00C0 | reg::OFFSET_00E8 | reg::OFFSET_04C8 | reg::OFFSET_04D0 => { + s.w[off as usize / 8] = value + } + reg::PROTECTION_KEY => { + if value == IIO_PROTECT_OVRRD_KEY { + s.w[0] |= 1 << self.cfg.request_region; + } + } + reg::CACHED_ALIAS => { + if s.w[5] != value & 15 { + s.w[5] = value & 15; + alias = Some((value & 15) as u8); + } + } + reg::OFFSET_0030 => s.w[6] = value & 255, + reg::OFFSET_0038 => s.w[7] = value & 0xffffff, + reg::CPU_A_ENABLE | reg::CPU_B_ENABLE => { + let slot = ((off - 0x50) / 8) as usize; + s.w[off as usize / 8] = value & 1; + enable = Some((slot, value & 1 != 0)); + } + reg::NMI_SLOT_A | reg::NMI_SLOT_B => { + if value & 1 != 0 { + nmi = Some(((off - 0x70) / 8) as usize); + } else { + self.note(off, true); + } + } + reg::PENDING_MODIFY | reg::PENDING_MODIFY_B => { + let now = s.time(now); + s.modify(value, true, now); + } + reg::REQUEST_A | reg::REQUEST_B => s.w[off as usize / 8] |= 1 << self.cfg.request_region, + reg::PENDING_A_CLEAR | reg::PENDING_B_CLEAR => s.w[(off - reg::OFFSET_0010) as usize / 8] &= !value, + reg::OFFSET_00F0 => { + s.w[0xf0 / 8] = value & 31; + if value & 31 != 0 { + self.note(off, true); + } + } + reg::RT_COMPARE_A | reg::RT_COMPARE_B => { + let slot = ((off - 0x108) / 8) as usize; + s.w[off as usize / 8] = value & RT_MASK; + s.arm_rt(slot, now); + compare = Some(slot); + } + reg::PROFILE_COMPARE => { + s.w[0x118 / 8] = value & 0xffffffff; + s.arm_profile(now); + } + reg::RT_STATUS_A | reg::RT_STATUS_B | reg::PROFILE_STATUS_A | reg::PROFILE_STATUS_B => s.w[off as usize / 8] = 0, + reg::RT_ENABLE_A | reg::RT_ENABLE_B | reg::PROFILE_ENABLE_A | reg::PROFILE_ENABLE_B => s.w[off as usize / 8] = value & 1, + reg::ERROR_SUMMARY_ALIAS => { + s.w[0x400 / 8] = + (s.w[0x400 / 8] & MEMORY_ERROR) | (value & ERROR_MASK & !MEMORY_ERROR) + } + reg::ERROR_SUMMARY => s.w[reg::ERROR_SUMMARY as usize / 8] &= !(value & ERROR_MASK), + reg::ERROR_CAUSE_A | reg::ERROR_CAUSE_B => s.w[off as usize / 8] = value & ERROR_MASK, + reg::ERROR_ADDRESS_A | reg::ERROR_ADDRESS_B => s.w[off as usize / 8] = value & 0x1fffffff8, + reg::ERROR_CONTROL => { + s.w[0x428 / 8] = value & 15; + if value & 15 != 0 { + self.note(off, true); + } + } + reg::ERROR_CAPTURE_A | reg::ERROR_CAPTURE_B | reg::ERROR_CAPTURE_C | reg::ERROR_CAPTURE_D => { + let slot = usize::from(off >= 0x450); + s.w[0x430 / 8 + slot * 4] = 0; + s.w[0x440 / 8 + slot * 4] = 0; + } + reg::ERROR_SOURCE_A | reg::ERROR_SOURCE_B => s.w[off as usize / 8] = value & 63, + reg::ERROR_DETAIL_A | reg::ERROR_DETAIL_B => s.w[off as usize / 8] = value & 0x1fffff, + reg::ERROR_DETAIL_C => s.w[off as usize / 8] = value & 0xfffff, + reg::CREDIT_MODE => s.credit_mode = value & 1, + reg::COUNTER => { + s.base = value & RT_MASK; + s.epoch = s.time(now); + s.arm_all(now); + } + _ => self.note(off, true), + } + drop(s); + if let Some(h) = self.hooks() { + if let Some(size) = alias { + h.cached_alias_changed(size); + } + if let Some(slot) = compare { + h.rt_compare_written(self.cfg.interface, slot); + } + if let Some((slot, value)) = enable { + h.cpu_enable(self.cfg.interface, slot, value); + } + if let Some(slot) = nmi { + h.nmi(self.cfg.interface, slot); + } + } + self.drive(); + self.schedule(); + } +} +fn readable(off: u64) -> bool { + matches!( + off, + reg::NODE_REGION | reg::OFFSET_0010 + | reg::OFFSET_0018 + | reg::CACHED_ALIAS + | reg::OFFSET_0030 + | reg::OFFSET_0038 + | reg::CPU_A_ENABLE + | reg::CPU_B_ENABLE + | reg::OFFSET_00A8 + | reg::OFFSET_00B0 + | reg::OFFSET_00B8 + | reg::OFFSET_00C0 + | reg::REQUEST_A + | reg::REQUEST_B + | reg::OFFSET_00E8 + | reg::OFFSET_00F0 + | reg::RT_COMPARE_A + | reg::RT_COMPARE_B + | reg::PROFILE_COMPARE + | reg::RT_STATUS_A + | reg::RT_STATUS_B + | reg::PROFILE_STATUS_A + | reg::PROFILE_STATUS_B + | reg::RT_ENABLE_A + | reg::RT_ENABLE_B + | reg::PROFILE_ENABLE_A + | reg::PROFILE_ENABLE_B + | reg::ERROR_SUMMARY + | reg::ERROR_CAUSE_A + | reg::ERROR_CAUSE_B + | reg::ERROR_ADDRESS_A + | reg::ERROR_ADDRESS_B + | reg::ERROR_CONTROL + | reg::OFFSET_0430 + | reg::OFFSET_0440 + | reg::OFFSET_0450 + | reg::OFFSET_0460 + | reg::ERROR_SOURCE_A + | reg::ERROR_DETAIL_A + | reg::ERROR_DETAIL_B + | reg::ERROR_DETAIL_C + | reg::ERROR_SOURCE_B + | reg::OFFSET_04C8 + | reg::OFFSET_04D0 + ) +} +impl BusDevice for BedrockPi { + fn read64(&self, addr: u64) -> BusRead64 { + if addr < SIZE && addr & 7 == 0 { + BusRead64::ok(self.read(addr)) + } else { + self.note(addr, false); + BusRead64::err() + } + } + fn read32(&self, addr: u64) -> BusRead32 { + if addr == reg::COUNTER || addr == reg::COUNTER + 4 { + let value = self.read(reg::COUNTER); + BusRead32::ok(if addr & 4 == 0 { + (value >> 32) as u32 + } else { + value as u32 + }) + } else { + self.note(addr, false); + BusRead32::err() + } + } + fn write64(&self, addr: u64, value: u64) -> u32 { + if addr < SIZE && addr & 7 == 0 { + self.write(addr, value); + BUS_OK + } else { + self.note(addr, true); + BUS_ERR + } + } +} +impl Device for BedrockPi { + fn start(&self) { + let now = (self.clock)(); + let mut s = self.state.lock(); + if s.running { + return; + } + if let Some(paused) = s.paused.take() { + let delta = now.saturating_sub(paused); + s.epoch = s.epoch.saturating_add(delta); + for due in &mut s.rt_due { + *due += u128::from(delta); + } + s.profile_due += u128::from(delta); + for due in &mut s.rearm { + if *due != 0 { + *due = due.saturating_add(delta); + } + } + } + s.running = true; + drop(s); + self.schedule(); + } + fn stop(&self) { + { + let _pass = self.timer_pass.lock(); + let mut s = self.state.lock(); + s.sync((self.clock)()); + s.paused = Some(s.time((self.clock)())); + s.running = false; + if let Some(id) = self.timer.lock().take() { + if let Some(tm) = self.timers.get() { + tm.remove(id); + } + } + } + self.drive(); + } + fn is_running(&self) -> bool { + self.state.lock().running + } + fn step(&self, _cycles: u64) { + self.poll(); + } + fn get_clock(&self) -> u64 { + self.state.lock().count((self.clock)()) + } +} +impl Resettable for BedrockPi { + fn power_on(&self) { + let _dispatch = self.dispatch.lock(); + let now = (self.clock)(); + let mut s = self.state.lock(); + // These controls and captures survive the PI block's local reset. + let old = s.w; + let running = s.running; + let reflected = s.reflected; + let uart = s.uart; + *s = State::new(now); + for off in [ + 0, 0x10, 0x18, 0x28, 0x50, 0x58, 0x400, 0x418, 0x420, 0x428, 0x430, 0x440, 0x450, + 0x460, 0x4c8, 0x4d0, + ] { + s.w[off / 8] = old[off / 8]; + } + s.running = running; + s.paused = if running { None } else { Some(now) }; + s.reflected = reflected; + s.uart = uart; + drop(s); + if let Some(h) = self.hooks() { + h.gfx_reset(); + for slot in 0..2 { + let enabled = self.state.lock().w[0x50 / 8 + slot] != 0; + h.cpu_enable(self.cfg.interface, slot, enabled); + } + } + self.drive(); + self.schedule(); + } +} +#[cfg(test)] +mod tests; diff --git a/src/dev/bedrock_pi/tests.rs b/src/dev/bedrock_pi/tests.rs new file mode 100644 index 000000000..5f684ad11 --- /dev/null +++ b/src/dev/bedrock_pi/tests.rs @@ -0,0 +1,276 @@ +// Cases follow tests/qtest/sn1-bedrock-pi-test.c in jamesbraid/qemu-sgi-origami: +// power-on-values, local-reset, pending-reflection, real-time-comparators +// and profile-counter-wrap. + +use super::*; +use std::sync::atomic::{AtomicU64, Ordering}; +fn setup() -> (Arc, Arc) { + let now = Arc::new(AtomicU64::new(0)); + let clock = now.clone(); + let pi = BedrockPi::new( + BedrockPiConfig::default(), + Arc::new(move || clock.load(Ordering::SeqCst)), + ); + pi.start(); + (pi, now) +} +fn r(pi: &BedrockPi, off: u64) -> u64 { + pi.read64(off).data +} +/// The PROM reads PI slot presence and relies on the documented reset survivors. +#[test] +fn the_pi_reports_interface_state_and_retains_registers_across_reset() { + { + let (pi, _) = setup(); + assert_eq!(r(&pi, reg::INTERFACE_ID), 0); + assert_eq!(r(&pi, reg::INTERFACE_SLOT_A), 1); + assert_eq!(r(&pi, reg::INTERFACE_SLOT_B), 0); + pi.write64(reg::NODE_REGION, 0); + pi.write64(reg::PROTECTION_KEY, 0x53474972756c6573); + assert_eq!(r(&pi, reg::NODE_REGION), 1); + let absent = BedrockPi::new( + BedrockPiConfig { + interface: 1, + request_region: 0, + }, + Arc::new(|| 0), + ); + assert_eq!(r(&absent, reg::INTERFACE_ID), 2); + assert_eq!(r(&absent, reg::INTERFACE_SLOT_A), 0); + assert_eq!(pi.read32(reg::INTERFACE_ID).status, BUS_ERR); + assert_eq!(pi.write32(reg::COUNTER, 0), BUS_ERR); + } + { + let (pi, _) = setup(); + pi.write64(reg::PENDING_MODIFY, 0x10c); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 1 << 12); + pi.write64(reg::PENDING_MODIFY_B, 12); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 0); + pi.set_lb_error(true); + pi.write64(reg::PENDING_MODIFY, 64 + 55); + assert_eq!(r(&pi, reg::PENDING_STATUS_B), 1 << 55); + pi.write64(reg::PENDING_MODIFY, 0x100 | 64 | 63); + assert_eq!(r(&pi, reg::PENDING_STATUS_B), 1 << 55); + pi.write64(reg::REQUEST_A, 0); + pi.write64(reg::OFFSET_00E8, 1); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 1 << 5); + pi.write64(reg::PENDING_A_CLEAR, 1); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 0); + pi.write64(reg::OFFSET_0010, 0); + assert!(!pi.io_modify(0x108, 0)); + pi.write64(reg::OFFSET_0010, 1); + assert!(pi.io_modify(0x108, 0)); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 1 << 8); + } + { + let (pi, _) = setup(); + pi.write64(reg::CACHED_ALIAS, 9); + pi.write64(reg::CPU_A_ENABLE, 1); + pi.write64(reg::ERROR_ADDRESS_A, !0); + pi.set_memory_uncorrectable(); + pi.write64(reg::ERROR_SUMMARY_ALIAS, 3); + pi.write64(reg::RT_COMPARE_A, 0x24); + pi.power_on(); + assert_eq!(r(&pi, reg::ERROR_SUMMARY), MEMORY_ERROR | 3); + assert_eq!(r(&pi, reg::CACHED_ALIAS), 9); + assert_eq!(r(&pi, reg::CPU_A_ENABLE), 1); + assert_eq!(r(&pi, reg::ERROR_ADDRESS_A), 0x1fffffff8); + assert_eq!(r(&pi, reg::RT_COMPARE_A), 0); + assert_eq!(r(&pi, reg::COUNTER), 0); + pi.write64(reg::ERROR_SUMMARY_ALIAS, 0); + assert_eq!(r(&pi, reg::ERROR_SUMMARY), MEMORY_ERROR); + pi.write64(reg::ERROR_SUMMARY, MEMORY_ERROR); + assert_eq!(r(&pi, reg::ERROR_SUMMARY), 0); + } +} +/// The guest timer drives interrupts even while the processor is stopped. +/// The processor timer can wrap, pause while stopped, and reissue its guest interrupt. +#[test] +fn the_pi_timer_wraps_pauses_and_reissues_interrupts() { + { + let (pi, now) = setup(); + pi.write64(reg::COUNTER, 0x1_0000_0000); + pi.write64(reg::RT_COMPARE_A, 0x1_0000_0002); + now.store(1599, Ordering::SeqCst); + assert_eq!(r(&pi, reg::RT_STATUS_A), 0); + now.store(1600, Ordering::SeqCst); + assert_eq!(r(&pi, reg::RT_STATUS_A), 1); + assert_eq!(pi.read32(reg::COUNTER).data, 1); + assert_eq!(pi.read32(reg::COUNTER + 4).data, 2); + pi.write64(reg::RT_STATUS_A, !0); + assert_eq!(r(&pi, reg::RT_STATUS_A), 0); + pi.write64(reg::COUNTER, RT_MASK); + pi.write64(reg::PROFILE_COMPARE, 0); + now.store(2400, Ordering::SeqCst); + assert_eq!(r(&pi, reg::COUNTER), 0); + assert_eq!(r(&pi, reg::PROFILE_STATUS_A), 1); + assert_eq!(r(&pi, reg::PROFILE_STATUS_B), 1); + pi.write64(reg::PROFILE_STATUS_A, 0); + assert_eq!(r(&pi, reg::PROFILE_STATUS_A), 0); + assert_eq!(r(&pi, reg::PROFILE_STATUS_B), 1); + } + { + let (pi, now) = setup(); + pi.set_level_source(0, 12, true); + pi.write64(reg::PENDING_MODIFY, 0x10c); + pi.write64(reg::PENDING_MODIFY, 12); + now.store(999999, Ordering::SeqCst); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 0); + now.store(1000000, Ordering::SeqCst); + assert_eq!(r(&pi, reg::PENDING_STATUS_A), 1 << 12); + pi.write64(reg::COUNTER, 0); + pi.write64(reg::RT_COMPARE_A, 2); + now.store(1000400, Ordering::SeqCst); + pi.stop(); + now.store(2000000, Ordering::SeqCst); + assert_eq!(r(&pi, reg::COUNTER), 0); + pi.start(); + now.store(2001200, Ordering::SeqCst); + assert_eq!(r(&pi, reg::RT_STATUS_A), 1); + } +} +/// An interrupt callback may inspect PI while an output changes. +/// An interrupt callback may inspect PI while an output changes. +#[test] +fn pi_interrupt_outputs_follow_masks_and_allow_reentrant_reads() { + struct Probe { + pi: Weak, + bits: AtomicU64, + } + impl InterruptController for Probe { + fn set_line(&self, line: u32, level: bool) { + if let Some(pi) = self.pi.upgrade() { + let _ = pi.read64(reg::PENDING_STATUS_A); + } + if level { + self.bits.fetch_or(1 << line, Ordering::SeqCst); + } else { + self.bits.fetch_and(!(1 << line), Ordering::SeqCst); + } + } + } + let (pi, now) = setup(); + let probe = Arc::new(Probe { + pi: Arc::downgrade(&pi), + bits: AtomicU64::new(0), + }); + pi.set_interrupt_controller(probe.clone()); + pi.write64(reg::OFFSET_00A8, 1 << 12); + pi.write64(reg::OFFSET_00B0, 1 << 55); + pi.write64(reg::PENDING_MODIFY, 0x10c); + pi.set_lb_error(true); + pi.write64(reg::RT_COMPARE_A, 2); + pi.write64(reg::PROFILE_COMPARE, 2); + pi.write64(reg::RT_ENABLE_A, 1); + pi.write64(reg::PROFILE_ENABLE_A, 1); + pi.write64(reg::ERROR_CAUSE_A, MEMORY_ERROR); + pi.set_memory_uncorrectable(); + now.store(1600, Ordering::SeqCst); + pi.poll(); + assert_eq!(probe.bits.load(Ordering::SeqCst), 31); + pi.write64(reg::RT_STATUS_A, 0); + assert_eq!(probe.bits.load(Ordering::SeqCst), 27); + pi.write64(reg::PENDING_MODIFY, 12); + pi.set_lb_error(false); + pi.write64(reg::PROFILE_STATUS_A, 0); + pi.write64(reg::ERROR_SUMMARY, MEMORY_ERROR); + assert_eq!(probe.bits.load(Ordering::SeqCst), 0); +} +/// A guest can wait for a timer interrupt without polling its compare register. +/// The guest may wait for a timer interrupt without polling the compare register. +#[test] +fn the_timer_manager_delivers_expirations_without_mmio_polling() { + struct Probe(std::sync::mpsc::Sender<()>); + impl InterruptController for Probe { + fn set_line(&self, line: u32, level: bool) { + if line == 2 && level { + self.0.send(()).unwrap(); + } + } + } + let pi = BedrockPi::new(BedrockPiConfig::default(), crate::dev::heart::host_clock()); + let tm = Arc::new(TimerManager::new()); + pi.set_timer_manager(tm.clone()); + let (tx, rx) = std::sync::mpsc::channel(); + pi.set_interrupt_controller(Arc::new(Probe(tx))); + pi.write64(reg::RT_COMPARE_A, 12500); + pi.write64(reg::RT_ENABLE_A, 1); + pi.start(); + rx.recv_timeout(Duration::from_secs(2)).unwrap(); + pi.stop(); + assert!(!pi.is_running()); +} +/// Node callbacks may reenter PI, so concurrent writes must keep their observed order. +/// Node callbacks may reenter PI, so concurrent writes must preserve observed order. +#[test] +fn concurrent_pi_updates_publish_callbacks_in_register_order() { + use std::sync::mpsc; + use std::thread; + + struct Hooks { + pi: Weak, + offset: u64, + first_value: u64, + entered: mpsc::Sender<()>, + release: Mutex>, + observed: AtomicU64, + } + impl Hooks { + fn publish(&self, value: u64) { + // A node may inspect PI registers while processing a change. + let _ = self.pi.upgrade().unwrap().read64(self.offset); + if value == self.first_value { + self.entered.send(()).unwrap(); + self.release.lock().recv().unwrap(); + } + self.observed.store(value, Ordering::SeqCst); + } + } + impl NodeHooks for Hooks { + fn cached_alias_changed(&self, size: u8) { + self.publish(u64::from(size)); + } + fn cpu_enable(&self, _: u8, _: usize, enabled: bool) { + self.publish(u64::from(enabled)); + } + } + + for (offset, first_value, second_value) in [(reg::CACHED_ALIAS, 1, 2), (reg::CPU_A_ENABLE, 1, 0), (reg::CPU_B_ENABLE, 1, 0)] { + let (pi, _) = setup(); + let (entered_tx, entered_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); + let hooks = Arc::new(Hooks { + pi: Arc::downgrade(&pi), + offset, + first_value, + entered: entered_tx, + release: Mutex::new(release_rx), + observed: AtomicU64::new(99), + }); + pi.set_node_hooks(hooks.clone()); + let first_pi = pi.clone(); + let first = thread::spawn(move || first_pi.write64(offset, first_value)); + entered_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + let (started_tx, started_rx) = mpsc::channel(); + let (done_tx, done_rx) = mpsc::channel(); + let second_pi = pi.clone(); + let second = thread::spawn(move || { + started_tx.send(()).unwrap(); + second_pi.write64(offset, second_value); + done_tx.send(()).unwrap(); + }); + started_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + let premature = done_rx.recv_timeout(Duration::from_millis(100)); + // Always release and join both writers before checking results. + release_tx.send(()).unwrap(); + assert_eq!(first.join().unwrap(), BUS_OK); + second.join().unwrap(); + assert_eq!( + hooks.observed.load(Ordering::SeqCst), + r(&pi, offset), + "offset {offset:#x}" + ); + assert!(matches!(premature, Err(mpsc::RecvTimeoutError::Timeout))); + assert_eq!(r(&pi, offset), second_value); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 8e5552271..b4ca3d18e 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -66,6 +66,7 @@ pub mod bedrock_xb; pub mod bedrock_lb; pub mod bedrock_md; pub mod bedrock_mem; +pub mod bedrock_pi; pub mod dp83843; pub mod ics1890; pub mod isp12160; From 6835774b3aac82995447bb1d57996b1c9c708476 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 12/20] bedrock_flash: add the Fuel boot flash windows Fuel reaches its AMD flash through executable PROM aliases and programming windows. Share the array bytes and persistent store while preserving the command and erase state of each flash part. The configuration block's CPU type word is 1, the R14000, as the IP35 PROM's own configuration editor lists it (0 for the R10000 and R12000, 2 for the R16000); a test pins it. --- src/dev/bedrock_flash.rs | 638 +++++++++++++++++++++++++++++++++++++++ src/dev/mod.rs | 1 + 2 files changed, 639 insertions(+) create mode 100644 src/dev/bedrock_flash.rs diff --git a/src/dev/bedrock_flash.rs b/src/dev/bedrock_flash.rs new file mode 100644 index 000000000..7df30a0cc --- /dev/null +++ b/src/dev/bedrock_flash.rs @@ -0,0 +1,638 @@ +//! Bedrock's boot flash: two independent 8 MiB arrays, selected 32 MiB +//! apart, with the AMD command set and CFI query data. +//! +//! Boot aliases and executable views share the same array bytes. Each array +//! has its own command state; instruction fetches read stored bytes even +//! while register reads return command status. +//! +//! Ported from hw/misc/sgi-bedrock-flash.c and hw/mips/sgi/initial-flash.c +//! in jamesbraid/qemu-sgi-origami. + + + +use super::sflash::amd::{AmdCore, Effect, SECTOR_SIZE}; +use super::sflash::storage::FlashBacking; +use crate::traits::{ + BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR, BUS_OK, +}; +use parking_lot::Mutex; +use std::io; +use std::path::Path; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; + +pub const PART_SIZE: usize = 8 << 20; +pub const FLASH_SIZE: usize = 2 * PART_SIZE; +pub const SECOND_POSITION: u64 = 32 << 20; + +fn sum(bytes: &[u8]) -> u8 { + bytes.iter().fold(0u8, |s, b| s.wrapping_add(*b)) +} + +/// The configuration block's CPU type word: 0 for the R10000 and R12000, 1 +/// for the R14000, 2 for the R16000, as the IP35 PROM's own configuration +/// editor lists them. The Fuel has an R14000. +const CPU_TYPE_R14000: u32 = 1; + +/// Apply Fuel's firmware record fields while retaining the configuration sum. +/// This policy also applies when reopening an existing persistent store. +fn configure(data: &mut [u8]) { + let config = &mut data[0x60..0xb0]; + let old = sum(config); + config[0x34..0x38].fill(0); + config[4..8].copy_from_slice(&0x549aa985u32.to_be_bytes()); + config[0x14..0x18].copy_from_slice(&500_000_000u32.to_be_bytes()); + config[0x30..0x34].copy_from_slice(&CPU_TYPE_R14000.to_be_bytes()); + config[0x38..0x3c].copy_from_slice(&1u32.to_be_bytes()); + let adjustment = old.wrapping_sub(sum(config)); + config[0x34..0x38].copy_from_slice(&(adjustment as u32).to_be_bytes()); +} + +struct State { + data: Vec, + parts: [AmdCore; 2], + backing: Option, +} + +impl State { + fn new(data: Vec) -> Self { + Self { + data, + parts: std::array::from_fn(|_| AmdCore::default()), + backing: None, + } + } + // Keep the in-memory array unchanged until each backing write succeeds. + // If a later range fails, restore the old bytes for all ranges already tried. + fn persist(&mut self, ranges: &[(usize, &[u8])]) -> io::Result<()> { + let Some(backing) = &mut self.backing else { + return Ok(()); + }; + for (index, &(offset, bytes)) in ranges.iter().enumerate() { + if let Err(error) = backing.write_range(offset, bytes) { + for &(restore_offset, restore_bytes) in &ranges[..=index] { + let old = &self.data[restore_offset..restore_offset + restore_bytes.len()]; + if let Err(rollback) = backing.write_range(restore_offset, old) { + log::error!( + "bedrock-flash: backing rollback at {restore_offset:#x} failed: {rollback}" + ); + } + } + return Err(error); + } + } + Ok(()) + } + fn replace(&mut self, offset: usize, bytes: &[u8]) -> io::Result { + if self.data[offset..offset + bytes.len()] == *bytes { + return Ok(false); + } + self.persist(&[(offset, bytes)])?; + self.data[offset..offset + bytes.len()].copy_from_slice(bytes); + Ok(true) + } + fn erase(&mut self, part: usize, sectors: &[usize]) -> io::Result { + let erased = vec![0xff; SECTOR_SIZE]; + let ranges: Vec<_> = sectors + .iter() + .map(|sector| (part * PART_SIZE + sector * SECTOR_SIZE, erased.as_slice())) + .collect(); + self.persist(&ranges)?; + let mut changed = false; + for (offset, bytes) in ranges { + let data = &mut self.data[offset..offset + bytes.len()]; + changed |= data.iter().any(|byte| *byte != 0xff); + data.fill(0xff); + } + Ok(changed) + } +} + +pub struct Flash { + st: Mutex, + gen: AtomicU64, +} + +impl Flash { + /// Format a fresh two-position store from a decoded payload. + pub fn new(payload: &[u8]) -> io::Result { + if payload.len() > PART_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Fuel PROM payload exceeds 8 MiB", + )); + } + let mut data = vec![0xff; FLASH_SIZE]; + data[..payload.len()].copy_from_slice(payload); + let label = PART_SIZE + 0x1e0000; + data[label + 0x10..label + 0x14].copy_from_slice(&0x504c4f49u32.to_be_bytes()); + data[label + 0x14..label + 0x18].copy_from_slice(&1u32.to_be_bytes()); + data[label + 0x18..label + 0x1c].copy_from_slice(&1u32.to_be_bytes()); + configure(&mut data); + Ok(Self { + st: Mutex::new(State::new(data)), + gen: AtomicU64::new(0), + }) + } + + /// Load an exact 16 MiB store, or seed a missing/empty one. Wrong sizes are + /// refused without writes. Reopening reapplies Fuel's configuration policy. + /// Completed guest programming and erasure write through to this file. + pub fn with_file(payload: &[u8], path: impl AsRef) -> io::Result { + let mut data = Self::new(payload)?.st.into_inner().data; + let mut backing = FlashBacking::open(path, &mut data).map_err(|error| { + if error.kind() == io::ErrorKind::InvalidData { + io::Error::new( + io::ErrorKind::InvalidData, + "Fuel flash store must be exactly 16 MiB (or empty)", + ) + } else { + error + } + })?; + configure(&mut data); + // Opening an existing store loads it before Fuel's configuration + // fields are refreshed; an empty store was seeded from this same data. + backing.write_range(0, &data)?; + let flash = Self { + st: Mutex::new(State { + data, + parts: std::array::from_fn(|_| AmdCore::default()), + backing: Some(backing), + }), + gen: AtomicU64::new(0), + }; + Ok(flash) + } + + pub fn contents(&self) -> Vec { + self.st.lock().data.clone() + } + pub fn generation(&self) -> u64 { + self.gen.load(Ordering::Acquire) + } + pub fn reset(&self) { + let mut st = self.st.lock(); + for part in &mut st.parts { + part.reset(); + } + self.gen.fetch_add(1, Ordering::Release); + } + /// Advance the model clock independently of flash reads. + pub fn advance_ns(&self, ns: u64) { + let mut st = self.st.lock(); + for part in 0..2 { + let selecting = st.parts[part].selecting(); + let complete = st.parts[part].advance_ns(ns); + if selecting && !st.parts[part].selecting() { + self.gen.fetch_add(1, Ordering::Release); + } + if !complete { + continue; + } + let sectors: Vec<_> = st.parts[part].selected_sectors().collect(); + let success = match st.erase(part, §ors) { + Ok(_) => true, + Err(e) => { + log::error!("bedrock-flash: position {part} erase backing write failed: {e}"); + false + } + }; + st.parts[part].finish_erase(success); + self.gen.fetch_add(1, Ordering::Release); + } + } + pub fn register_locks(self: &Arc) { + let me = self.clone(); + crate::locks::register_lock_fn("bedrock_flash::data", move || me.st.is_locked()); + } + /// Use the same shared flash for local and remote boot spaces. + pub fn boot_view(self: &Arc) -> FlashView { + FlashView { + flash: self.clone(), + range: None, + } + } + /// A contiguous executable alias. Offsets refer to the 16 MiB backing. + pub fn array_view(self: &Arc, offset: usize, len: usize) -> io::Result { + if offset.checked_add(len).is_none_or(|end| end > FLASH_SIZE) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Fuel flash alias exceeds backing", + )); + } + Ok(FlashView { + flash: self.clone(), + range: Some((offset, len)), + }) + } +} +impl Resettable for Flash { + fn power_on(&self) { + self.reset(); + } +} + +pub struct FlashView { + flash: Arc, + range: Option<(usize, usize)>, +} +impl FlashView { + fn offset(&self, addr: u64, width: usize) -> Option { + if let Some((base, len)) = self.range { + let off = usize::try_from(addr).ok()?; + return off + .checked_add(width) + .filter(|end| *end <= len) + .map(|_| base + off); + } + let (base, within) = if addr < PART_SIZE as u64 { + (0, addr) + } else if addr >= SECOND_POSITION { + (PART_SIZE, addr - SECOND_POSITION) + } else { + return None; + }; + let within = usize::try_from(within).ok()?; + within + .checked_add(width) + .filter(|end| *end <= PART_SIZE) + .map(|_| base + within) + } + fn read(&self, addr: u64, width: usize) -> Option { + let off = self.offset(addr, width)?; + let mut st = self.flash.st.lock(); + if self.range.is_none() { + let part = off / PART_SIZE; + if !st.parts[part].array_reads() && (width != 2 || off & 1 != 0) { + return None; + } + if width == 2 && off & 1 == 0 { + if let Some(value) = st.parts[part].read16(off % PART_SIZE) { + return Some(value as u64); + } + } + } + Some( + st.data[off..off + width] + .iter() + .fold(0, |v, b| (v << 8) | *b as u64), + ) + } + fn refuse(&self, addr: u64, width: usize, value: u64) -> u32 { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "bedrock-flash: unsupported {width}-byte store {value:#x} at {addr:#x}; invalid bus cycle or executable view" + ); + BUS_ERR + } +} +impl BusDevice for FlashView { + fn read8(&self, a: u64) -> BusRead8 { + self.read(a, 1) + .map_or_else(BusRead8::err, |v| BusRead8::ok(v as u8)) + } + fn read16(&self, a: u64) -> BusRead16 { + self.read(a, 2) + .map_or_else(BusRead16::err, |v| BusRead16::ok(v as u16)) + } + fn read32(&self, a: u64) -> BusRead32 { + self.read(a, 4) + .map_or_else(BusRead32::err, |v| BusRead32::ok(v as u32)) + } + fn read64(&self, a: u64) -> BusRead64 { + self.read(a, 8).map_or_else(BusRead64::err, BusRead64::ok) + } + fn write8(&self, a: u64, v: u8) -> u32 { + self.refuse(a, 1, v as u64) + } + fn write16(&self, a: u64, v: u16) -> u32 { + self.refuse(a, 2, v as u64) + } + fn write32(&self, a: u64, v: u32) -> u32 { + self.refuse(a, 4, v as u64) + } + fn write64(&self, a: u64, v: u64) -> u32 { + if self.range.is_some() || a & 7 != 0 || v > u16::MAX as u64 { + return self.refuse(a, 8, v); + } + let (part, within) = if a < SECOND_POSITION { + (0, a) + } else { + (1, a - SECOND_POSITION) + }; + let Some(end) = within.checked_add(8) else { + return self.refuse(a, 8, v); + }; + if end > SECOND_POSITION { + return self.refuse(a, 8, v); + } + let offset = (within / 4) as usize; + let mut st = self.flash.st.lock(); + let effect = match st.parts[part].write16(offset, v as u16) { + Ok(effect) => effect, + Err(()) => { + self.flash.gen.fetch_add(1, Ordering::Release); + return BUS_ERR; + } + }; + if let Effect::Program { offset, value } = effect { + let backing = part * PART_SIZE + offset; + let old = u16::from_be_bytes([st.data[backing], st.data[backing + 1]]); + match st.replace(backing, &(old & value).to_be_bytes()) { + Ok(_) => {} + Err(e) => { + log::error!("bedrock-flash: program backing write at {backing:#x} failed: {e}"); + self.flash.gen.fetch_add(1, Ordering::Release); + return BUS_ERR; + } + } + } + self.flash.gen.fetch_add(1, Ordering::Release); + BUS_OK + } + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, _addr: u64) -> *const AtomicU64 { + &self.flash.gen + } +} + +#[cfg(test)] +mod tests { + // Cases follow tests/qtest/sn1-bedrock-flash-test.c in + // jamesbraid/qemu-sgi-origami: program, sector-erase, erase-polling, + // reset-states and backing-file. + use super::*; + fn unlock(v: &FlashView, part: u64) { + assert_eq!(v.write64(part + 8 * 0x555, 0xaa), crate::traits::BUS_OK); + assert_eq!(v.write64(part + 8 * 0x2aa, 0x55), crate::traits::BUS_OK); + } + fn program(v: &FlashView, part: u64, off: u64, value: u16) { + unlock(v, part); + assert_eq!(v.write64(part + 8 * 0x555, 0xa0), crate::traits::BUS_OK); + assert_eq!( + v.write64(part + 4 * off, value as u64), + crate::traits::BUS_OK + ); + } + fn erase_select(v: &FlashView, part: u64, offset: u64) { + unlock(v, part); + assert_eq!(v.write64(part + 8 * 0x555, 0x80), BUS_OK); + unlock(v, part); + assert_eq!(v.write64(part + 4 * offset, 0x30), BUS_OK); + } + + /// The PROM reads CPU configuration and the volume label from its boot flash. + #[test] + fn the_seed_exposes_fuel_configuration_and_volume_label() { + let seed = vec![0x21; 0xb0]; + let f = Flash::new(&seed).unwrap(); + let bytes = f.contents(); + assert_eq!(sum(&bytes[0x60..0xb0]), sum(&seed[0x60..0xb0])); + assert_eq!(&bytes[0x64..0x68], &0x549aa985u32.to_be_bytes()); + assert_eq!(&bytes[0x74..0x78], &500_000_000u32.to_be_bytes()); + assert_eq!(&bytes[0x90..0x94], &1u32.to_be_bytes(), "CPU type: R14000"); + let label = PART_SIZE + 0x1e0000; + assert_eq!(&bytes[label..label + 16], &[0xff; 16]); + assert_eq!( + &bytes[label + 16..label + 28], + &[0x50, 0x4c, 0x4f, 0x49, 0, 0, 0, 1, 0, 0, 0, 1] + ); + assert!(bytes[label + 28..].iter().all(|b| *b == 0xff)); + assert!(Flash::new(&vec![0; PART_SIZE + 1]).is_err()); + } + /// The PROM programs through one view while executable aliases must + /// keep reading array bytes. + #[test] + fn programming_changes_shared_bytes_without_exposing_status_to_instruction_fetches() { + { + let f = Arc::new(Flash::new(&[]).unwrap()); + let local = f.boot_view(); + let remote = f.boot_view(); + let execute = f.array_view(0, PART_SIZE).unwrap(); + assert_eq!(local.write64(SECOND_POSITION, 0xf0), crate::traits::BUS_OK); + for part in [0, SECOND_POSITION] { + assert_eq!(local.write64(part + 8 * 0x55, 0x98), crate::traits::BUS_OK); + assert_eq!(remote.read16(part + 0x20).data, b'Q' as u16); + assert_eq!(remote.read16((SECOND_POSITION - part) + 0x20).data, 0xffff); + assert_eq!(execute.read16(0x20).data, 0xffff); + assert_eq!(remote.write64(part, 0xf0), crate::traits::BUS_OK); + assert_eq!(local.write64(part + 8 * 0x555, 0xaa), crate::traits::BUS_OK); + assert_eq!( + remote.write64(part + 8 * 0x2aa, 0x55), + crate::traits::BUS_OK + ); + assert_eq!(local.write64(part + 8 * 0x555, 0xa0), crate::traits::BUS_OK); + assert_eq!( + remote.write64(part + 4 * 0x200, 0x1234), + crate::traits::BUS_OK + ); + assert_eq!(local.read16(part + 0x200).data, 0x1234); + } + assert_eq!(execute.read16(0x200).data, 0x1234); + assert!(!local.read8(PART_SIZE as u64).is_ok()); + } + { + let f = Arc::new(Flash::new(&[]).unwrap()); + let v = f.boot_view(); + program(&v, 0, 0x200, 0x1234); + program(&v, 0, 0x200, 0xf0ff); + assert_eq!(v.read16(0x200).data, 0x1034); + unlock(&v, 0); + assert_eq!(v.write64(8 * 0x555, 0xa0), crate::traits::BUS_OK); + let gen = f.generation(); + for (addr, value) in [ + (0x800, 0x10000), + (0x804, 0), + (64 << 20, 0), + ] { + assert_eq!(v.write64(addr, value), BUS_ERR); + } + assert_eq!(v.write16(0x800, 0), BUS_ERR); + assert_eq!(f.generation(), gen); + assert_eq!(v.write64(0x800, 0x00ff), crate::traits::BUS_OK); + assert_eq!(v.read16(0x200).data, 0x0034); + let execute = f.array_view(0, PART_SIZE).unwrap(); + unlock(&v, 0); + assert_eq!(execute.write64(8 * 0x555, 0xa0), BUS_ERR); + assert_eq!(v.write64(8 * 0x555, 0xa0), crate::traits::BUS_OK); + assert_eq!(v.write64(0x808, 0x5555), crate::traits::BUS_OK); + assert_eq!(execute.read16(0x202).data, 0x5555); + } + { + let f = Arc::new(Flash::new(&[]).unwrap()); + let v = f.boot_view(); + assert_eq!(v.write64(0x550, 0x98), BUS_ERR); + assert_eq!(v.read16(0x20).data, 0xffff); + assert_eq!(v.write64(8 * 0x55, 0x98), BUS_OK); + let expected = [ + 0x51, 0x52, 0x59, 2, 0, 0, 0, 0, 0, 0, 0, 0x27, 0x36, 0, 0, 3, 0, 9, 0, 4, 0, 4, 0, 23, + 1, 0, 0, 0, 1, 0x7f, 0, 0, 1, + ]; + for (index, value) in expected.into_iter().enumerate() { + assert_eq!(v.read16(((index + 0x10) * 2) as u64).data, value); + } + assert_eq!(v.read16(0x100).data, 0); + assert!(!v.read8(0x20).is_ok()); + assert!(!v.read16(0x21).is_ok()); + f.reset(); + assert_eq!(v.read16(0x20).data, 0xffff); + } + } + /// The PROM polls erase completion and can reset the chip before + /// selected sectors change. + #[test] + fn erase_completes_only_for_selected_sectors_and_reset_cancels_it() { + let f = Arc::new(Flash::new(&[]).unwrap()); + let v = f.boot_view(); + program(&v, 0, 0x200, 0); + program(&v, 0, 0x10000, 0); + erase_select(&v, 0, 0x200); + assert!(!v.read8(0x200).is_ok()); + let a = v.read16(0x200).data; + let b = v.read16(0x200).data; + assert_eq!((a ^ b) & 0x44, 0x44); + let a = v.read16(0x10000).data; + let b = v.read16(0x10000).data; + assert_eq!((a ^ b) & 0x44, 0x40); + f.advance_ns(40_000); + assert_eq!(v.write64(4 * 0x10000, 0x30), BUS_OK); + f.advance_ns(40_000); + assert_eq!(v.read16(0x10000).data & 8, 0); + assert_eq!(v.write64(SECOND_POSITION + 8 * 0x55, 0x98), BUS_OK); + assert_eq!(v.read16(SECOND_POSITION + 0x20).data, b'Q' as u16); + f.advance_ns(10_000); + assert_eq!(v.read16(0x200).data & 8, 8); + assert_eq!(v.write64(0, 0xf0), BUS_OK); + assert_eq!(v.read16(0x200).data & 0x80, 0); + f.reset(); + f.advance_ns(u64::MAX); + assert_eq!(v.read16(0x200).data, 0); + assert_eq!(v.read16(0x10000).data, 0); + assert_eq!(v.read16(SECOND_POSITION + 0x20).data, 0xffff); + erase_select(&v, 0, 0x200); + assert_eq!(v.write64(0, 0xf0), BUS_OK); + f.advance_ns(u64::MAX); + assert_eq!(v.read16(0x200).data, 0); + erase_select(&v, 0, 0x200); + assert_eq!(v.write64(4 * 0x10000, 0x30), BUS_OK); + f.advance_ns(50_000 + 512_000_000); + assert_eq!(v.read16(0x200).data, 0xffff); + assert_eq!(v.read16(0x10000).data, 0xffff); + unlock(&v, 0); + assert_eq!(v.write64(8 * 0x555, 0xa0), BUS_OK); + f.reset(); + assert_eq!(v.write64(4 * 0x200, 0x1234), BUS_ERR); + assert_eq!(v.read16(0x200).data, 0xffff); + program(&v, 0, 0x200, 0); + program(&v, 0, 0x10000, 0x3456); + erase_select(&v, 0, 0x200); + f.advance_ns(50_000 + 512_000_000); + assert_eq!(v.read16(0x200).data, 0xffff); + assert_eq!(v.read16(0x10000).data, 0x3456); + } + /// Firmware updates must persist across emulator restarts without + /// overwriting a supplied image. + #[test] + fn completed_programming_and_erases_survive_reopening_the_backing_file() { + { + let path = std::env::temp_dir().join(format!("iris-bedrock-store-{}", std::process::id())); + let mut bytes = vec![0x5a; FLASH_SIZE]; + bytes[PART_SIZE] = 0xc7; + std::fs::write(&path, &bytes).unwrap(); + let f = Flash::with_file(&[0x11], &path).unwrap(); + assert_eq!(f.contents()[0], 0x5a); + assert_eq!(f.contents()[PART_SIZE], 0xc7); + assert_eq!(std::fs::read(&path).unwrap(), f.contents()); + drop(f); + std::fs::write(&path, [1, 2, 3]).unwrap(); + assert!(Flash::with_file(&[], &path).is_err()); + assert_eq!(std::fs::read(&path).unwrap(), [1, 2, 3]); + std::fs::write(&path, []).unwrap(); + let g = Flash::with_file(&[0x34], &path).unwrap(); + assert_eq!(g.contents()[0], 0x34); + assert_eq!(std::fs::metadata(&path).unwrap().len(), FLASH_SIZE as u64); + drop(g); + std::fs::remove_file(path).unwrap(); + } + { + let path = std::env::temp_dir().join(format!("iris-amd-persist-{}", std::process::id())); + std::fs::write(&path, []).unwrap(); + let f = Arc::new(Flash::with_file(&[], &path).unwrap()); + let v = f.boot_view(); + program(&v, 0, 0x200, 0x1234); + program(&v, SECOND_POSITION, (PART_SIZE - 2) as u64, 0x5678); + drop(v); + drop(f); + let f = Arc::new(Flash::with_file(&[], &path).unwrap()); + let v = f.boot_view(); + assert_eq!(v.read16(0x200).data, 0x1234); + assert_eq!( + v.read16(SECOND_POSITION + PART_SIZE as u64 - 2).data, + 0x5678 + ); + unlock(&v, SECOND_POSITION); + assert_eq!(v.write64(SECOND_POSITION + 8 * 0x555, 0x80), BUS_OK); + unlock(&v, SECOND_POSITION); + assert_eq!( + v.write64(SECOND_POSITION + 4 * (PART_SIZE as u64 - 2), 0x30), + BUS_OK + ); + f.advance_ns(50_000 + 512_000_000); + drop(v); + drop(f); + let f = Arc::new(Flash::with_file(&[], &path).unwrap()); + let v = f.boot_view(); + assert_eq!(v.read16(0x200).data, 0x1234); + assert_eq!( + v.read16(SECOND_POSITION + PART_SIZE as u64 - 2).data, + 0xffff + ); + std::fs::remove_file(path).unwrap(); + } + } + + /// A host write failure must preserve flash contents and remain visible + /// to the guest. + #[test] + fn failed_backing_writes_leave_array_bytes_unchanged() { + { + let f = Arc::new(Flash::new(&[]).unwrap()); + // A read-only descriptor makes writes fail without a special backend. + let path = std::env::temp_dir().join(format!("iris-amd-read-only-{}", std::process::id())); + std::fs::write(&path, []).unwrap(); + f.st.lock().backing = Some(FlashBacking::from_file( + std::fs::File::open(&path).unwrap(), + FLASH_SIZE, + )); + let v = f.boot_view(); + unlock(&v, 0); + assert_eq!(v.write64(8 * 0x555, 0xa0), BUS_OK); + assert_eq!(v.write64(4 * 0x200, 0x1234), BUS_ERR); + assert_eq!(v.read16(0x200).data, 0xffff); + std::fs::remove_file(path).unwrap(); + } + { + let f = Arc::new(Flash::new(&[]).unwrap()); + let v = f.boot_view(); + program(&v, 0, 0x200, 0); + let path = + std::env::temp_dir().join(format!("iris-amd-erase-failure-{}", std::process::id())); + std::fs::write(&path, []).unwrap(); + f.st.lock().backing = Some(FlashBacking::from_file( + std::fs::File::open(&path).unwrap(), + FLASH_SIZE, + )); + erase_select(&v, 0, 0x200); + f.advance_ns(50_000 + 512_000_000); + assert_eq!(v.read16(0x200).data & 0x20, 0x20); + assert_eq!(f.contents()[0x200], 0); + assert_eq!(v.write64(0, 0xf0), BUS_OK); + assert_eq!(v.read16(0x200).data, 0); + std::fs::remove_file(path).unwrap(); + } + } + +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index b4ca3d18e..a0a5d060b 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -60,6 +60,7 @@ pub mod vino; pub mod virtio; pub mod wd33c93a; pub mod z85c30; +pub mod bedrock_flash; pub mod bedrock_io; pub mod bedrock_ni; pub mod bedrock_xb; From 04d50c72379881a90572cba68f0a0ab27005ef98 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 13/20] xbridge: add the two-head Fuel PCI host bridge Fuel routes PCI configuration, DMA and interrupt packets through two XBridge heads. Keep small sparse PCI decode in window lists so the bridge can share the machine memory map without allocating more Physical tables. A board attached to one of the crossbar's external ports raises its interrupts as crosstalk packets, the same way the PCI heads do. Route them through the same sender, so the Fuel's graphics board reaches the Bedrock's processor interface with no path of its own, and say whether the destination took the packet. The crossbar answers with the Crossbow's widget 0 register map, so it takes its offsets from the Octane's Crossbow model instead of restating them. What stays separate is behavior that differs between the two: the XBridge crossbar's status register carries the requesting port, wide and malformed accesses set the access-error status and answer zero, its interrupts go out as crosstalk packets to any destination, and it resets the heads and ports its fences select. --- src/dev/mod.rs | 1 + src/dev/xbow.rs | 68 +- src/dev/xbridge.rs | 1600 ++++++++++++++++++++++++++++++++++++++ src/dev/xbridge/tests.rs | 738 ++++++++++++++++++ 4 files changed, 2373 insertions(+), 34 deletions(-) create mode 100644 src/dev/xbridge.rs create mode 100644 src/dev/xbridge/tests.rs diff --git a/src/dev/mod.rs b/src/dev/mod.rs index a0a5d060b..db1d9f738 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -72,3 +72,4 @@ pub mod dp83843; pub mod ics1890; pub mod isp12160; pub mod timekeeper; +pub mod xbridge; diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index 85f395255..2a6a2b9dd 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -39,51 +39,51 @@ pub const fn widget_id(rev: u32, part: u32, mfgr: u32) -> u32 { } /// The widget HEART answers as, where widget 0's interrupt packets go. -const WIDGET_HEART: u8 = 8; +pub(crate) const WIDGET_HEART: u8 = 8; -const FIRST_WIDGET: u8 = 8; -const LINKS: usize = 8; +pub(crate) const FIRST_WIDGET: u8 = 8; +pub(crate) const LINKS: usize = 8; /// The register block's span in widget 0's window. pub const REG_SPAN: u32 = 0x0100_0000; -const IDENTIFICATION: u32 = 0x00; -const STATUS: u32 = 0x08; -const ERR_UPPER: u32 = 0x10; -const ERR_LOWER: u32 = 0x18; -const CONTROL: u32 = 0x20; -const PACKET_TIMEOUT: u32 = 0x28; -const INT_UPPER: u32 = 0x30; -const INT_LOWER: u32 = 0x38; -const ERR_CMD: u32 = 0x40; -const LLP_CONTROL: u32 = 0x48; -const STATUS_CLEAR: u32 = 0x50; -const ARB_RELOAD: u32 = 0x58; -const PERF_A: u32 = 0x60; -const PERF_B: u32 = 0x68; -const NIC: u32 = 0x70; +pub(crate) const IDENTIFICATION: u32 = 0x00; +pub(crate) const STATUS: u32 = 0x08; +pub(crate) const ERR_UPPER: u32 = 0x10; +pub(crate) const ERR_LOWER: u32 = 0x18; +pub(crate) const CONTROL: u32 = 0x20; +pub(crate) const PACKET_TIMEOUT: u32 = 0x28; +pub(crate) const INT_UPPER: u32 = 0x30; +pub(crate) const INT_LOWER: u32 = 0x38; +pub(crate) const ERR_CMD: u32 = 0x40; +pub(crate) const LLP_CONTROL: u32 = 0x48; +pub(crate) const STATUS_CLEAR: u32 = 0x50; +pub(crate) const ARB_RELOAD: u32 = 0x58; +pub(crate) const PERF_A: u32 = 0x60; +pub(crate) const PERF_B: u32 = 0x68; +pub(crate) const NIC: u32 = 0x70; /// Revision code 5 and later: widget 0's reset fence, then links 8 to 15's /// at a stride of 8, then the resource lock and its clear. -const FENCE_BASE: u32 = 0x78; -const FENCES: usize = 1 + LINKS; -const FENCE_END: u32 = FENCE_BASE + 8 * FENCES as u32; -const LOCK: u32 = 0xc0; -const LOCK_CLEAR: u32 = 0xc8; +pub(crate) const FENCE_BASE: u32 = 0x78; +pub(crate) const FENCES: usize = 1 + LINKS; +pub(crate) const FENCE_END: u32 = FENCE_BASE + 8 * FENCES as u32; +pub(crate) const LOCK: u32 = 0xc0; +pub(crate) const LOCK_CLEAR: u32 = 0xc8; const FENCE_MASK: u32 = 0xff; /// The first revision code with fences and the lock (Crossbow 2.0 in the /// software tables; an Octane2's code-5 part shows as 1.4 in its inventory). const REVISION_FENCES: u32 = 5; -const LINK_BASE: u32 = 0x100; -const LINK_STRIDE: u32 = 0x40; -const LINK_END: u32 = LINK_BASE + LINK_STRIDE * LINKS as u32; -const LINK_FLUSH: u32 = 0x00; -const LINK_CONTROL: u32 = 0x08; -const LINK_STATUS: u32 = 0x10; -const LINK_ARB_UPPER: u32 = 0x18; -const LINK_ARB_LOWER: u32 = 0x20; -const LINK_STATUS_CLEAR: u32 = 0x28; -const LINK_RESET: u32 = 0x30; -const LINK_AUX_STATUS: u32 = 0x38; +pub(crate) const LINK_BASE: u32 = 0x100; +pub(crate) const LINK_STRIDE: u32 = 0x40; +pub(crate) const LINK_END: u32 = LINK_BASE + LINK_STRIDE * LINKS as u32; +pub(crate) const LINK_FLUSH: u32 = 0x00; +pub(crate) const LINK_CONTROL: u32 = 0x08; +pub(crate) const LINK_STATUS: u32 = 0x10; +pub(crate) const LINK_ARB_UPPER: u32 = 0x18; +pub(crate) const LINK_ARB_LOWER: u32 = 0x20; +pub(crate) const LINK_STATUS_CLEAR: u32 = 0x28; +pub(crate) const LINK_RESET: u32 = 0x30; +pub(crate) const LINK_AUX_STATUS: u32 = 0x38; const STATUS_REQUEST: u32 = 1 << 23; const STATUS_LINK_REQUESTS: u32 = 0xff00_0000; diff --git a/src/dev/xbridge.rs b/src/dev/xbridge.rs new file mode 100644 index 000000000..7025b3b20 --- /dev/null +++ b/src/dev/xbridge.rs @@ -0,0 +1,1600 @@ +//! Fuel's XBridge revision 2 contains two independent PCI heads and their +//! shared crossbar. Each head has its own 1024-entry ATE, device windows and +//! interrupt state. Revision 2 places aliases differently from the Octane's +//! single-head Bridge, uses different PCI slot data-phase widths, and sends +//! host DMA through widget 0 with source-aware packets. +//! +//! This remains separate from `bridge.rs` because its two-head topology, +//! alias map and host packet path are externally visible parts of this chip. +//! Generic PCI BAR-run and lane helpers are shared; the ATE and interrupt +//! dispatch remain tied to the XBridge head layout. Sorted BAR runs decode PCI +//! PIO without a `Physical` table; the machine's RAM still uses one. +//! +//! # What is approximated here +//! +//! Slot widths, immediate link initialization, synchronous flushes and +//! revision-2 alias placement follow the project's BSD QEMU model, not a +//! silicon measurement. Timing, external SRAM, flash and diagnostic engines +//! are absent. Undefined accesses latch an error and remain observable. +//! +//! Ported from hw/pci-host/sgi-xbridge.c and hw/misc/sgi-xbridge-xbow.c +//! in jamesbraid/qemu-sgi-origami, revision +//! fe128635b6dfe2511f1bd451684cb30007686df1. + +use crate::dev::w1; +use crate::irq::{InterruptController, IrqLine}; +use crate::pci::{bar_runs, BarRun, PciBar, PciBusDevice, PciDma, PciDmaStatus, PciLanes, Space}; +use crate::physical::{read_bytes, write_bytes}; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_OK}; +use parking_lot::{Mutex, RwLock}; +use std::collections::HashMap; +use std::sync::{atomic::{AtomicU64, Ordering}, Arc, Weak}; + +pub const HOST_WIDGET: u8 = 10; +pub const HEAD_E: u8 = 14; +pub const HEAD_F: u8 = 15; +pub const ATE_ENTRIES: usize = 1024; +pub const ATE_MAPPED: usize = 512; + +/// Offsets of the PCI head register words, at eight-byte strides. +pub mod reg { + pub const IDENTIFICATION: usize = 0x0; + pub const STATUS: usize = 0x8; + pub const ERR_UPPER: usize = 0x10; + pub const ERR_LOWER: usize = 0x18; + pub const CONTROL: usize = 0x20; + pub const REQUEST_TIMEOUT: usize = 0x28; + pub const INT_DEST_UPPER: usize = 0x30; + pub const INT_DEST_LOWER: usize = 0x38; + pub const LINK_CONFIG: usize = 0x48; + pub const TEST_PIN_CONTROL: usize = 0x70; + pub const DIRECT_MAP: usize = 0x80; + pub const SSRAM_PARITY_ERR: usize = 0x90; + pub const ARBITRATION: usize = 0xa0; + pub const NIC: usize = 0xb0; + pub const BUS_TIMEOUT: usize = 0xc0; + pub const PCI_TYPE1_CONFIG: usize = 0xc8; + pub const PCI_ERR_LOWER: usize = 0xd8; + pub const INTERRUPT_STATUS: usize = 0x100; + pub const INTERRUPT_ENABLE: usize = 0x108; + pub const INTERRUPT_RESET: usize = 0x110; + pub const INTERRUPT_MODE: usize = 0x118; + pub const INTERRUPT_DEVICE: usize = 0x120; + pub const INTERRUPT_ERROR_HOST: usize = 0x128; + pub const INTERRUPT_HOST_BASE: usize = 0x130; + pub const INTERRUPT_HOST_LAST: usize = 0x168; + pub const INTERRUPT_MULTIPLE: usize = 0x170; + pub const FORCE_INTERRUPT_BASE: usize = 0x180; + pub const FORCE_PIN_BASE: usize = 0x1c0; + pub const FORCE_PIN_LAST: usize = 0x1f8; + pub const DEVICE_BASE: usize = 0x200; + pub const DEVICE_LAST: usize = 0x238; + pub const RRB_EVEN: usize = 0x280; + pub const RRB_ODD: usize = 0x288; + pub const RRB_CLEAR: usize = 0x298; + pub const ATE_UPPER_BASE: u64 = 0x10000; + pub const ATE_LOWER_BASE: u64 = 0x12000; + pub const ATE_END: u64 = 0x14000; + +} + +/// The crossbar has the Crossbow's widget 0 register map, which `xbow.rs` +/// owns; the register offsets here are its constants. The access rules and +/// reset values below are this chip's own (the Octane's Crossbow model differs +/// in status, link state, wide accesses and where its interrupts go). +use crate::dev::xbow; + +/// Fences run from widget 0's to link 15's. +const FENCE_LAST: u32 = xbow::FENCE_END - 8; +/// Read-only registers between the resource lock and the first link. +const READ_ONLY_BASE: u32 = xbow::LOCK_CLEAR + 8; +const READ_ONLY_LAST: u32 = xbow::LINK_BASE - 8; +fn ix(off: u32) -> usize { + (off / 8) as usize +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InterruptPacket { + pub source_widget: u8, + pub widget: u8, + pub address: u64, + pub vector: u8, + pub level: bool, +} +pub type InterruptSender = Arc bool + Send + Sync>; + +/// Source identity and placement hints retained for a host's inbound packet path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DmaPacket { + pub source_widget: u8, + pub slot: u8, + pub widget: u8, + pub address: u64, + pub write: bool, + pub half_write_requested: bool, + pub half_read_requested: bool, +} +/// Bytes are in XIO address order after bridge placement. A false return +/// refuses that access. The host applies the requested exchange of doubleword halves. +pub type DmaSender = Arc bool + Send + Sync>; + +struct Routes { + targets: RwLock<[Option>; 16]>, + interrupts: RwLock>, + dma: RwLock>, +} +impl Routes { + fn interrupt(&self, p: InterruptPacket) -> bool { + let sender = self.interrupts.read().clone(); + let accepted = if let Some(send) = sender { + send(p) + } else { + let target = self.targets.read()[p.widget as usize].clone(); + target.is_some_and(|t| { + t.write64( + p.address, + p.vector as u64 | if p.level { 1 << 8 } else { 0 }, + ) == BUS_OK + }) + }; + if !accepted { + crate::dlog!(crate::devlog::LogModule::Bedrock, "xbridge: refused interrupt {p:?}"); + } + accepted + } +} + +/// One chip owns both heads and widget 0; views share routing and reset state. +pub struct XBridge { + routes: Arc, + heads: [Arc; 2], + xbow: Arc, +} +impl XBridge { + pub fn new(memory: Arc) -> Arc { + let routes = Arc::new(Routes { + targets: RwLock::new(std::array::from_fn(|i| { + if i == HOST_WIDGET as usize { + Some(memory.clone()) + } else { + None + } + })), + interrupts: RwLock::new(None), + dma: RwLock::new(None), + }); + let heads = [ + XBridgeHead::new(HEAD_E, 1, routes.clone()), + XBridgeHead::new(HEAD_F, 0, routes.clone()), + ]; + let xbow = Arc::new(Crossbar { + st: Mutex::new(XbowState::new()), + order: Mutex::new(()), + routes: routes.clone(), + heads: heads.each_ref().map(Arc::downgrade), + reset_output: RwLock::new(None), + }); + Arc::new(Self { + routes, + heads, + xbow, + }) + } + pub fn register_locks(self: &Arc) { + use crate::locks::register_lock_fn; + for head in &self.heads { + for (what, probe) in [ + ("state", (|h: &XBridgeHead| h.st.is_locked()) as fn(&XBridgeHead) -> bool), + ("irq_order", |h| h.order.is_locked()), + ("microlan", |h| h.microlan.is_locked()), + ("pio_map", |h| h.pio_map.is_locked()), + ] { + let h = Arc::downgrade(head); + let name = format!("xbridge::head{:x}::{what}", head.widget); + register_lock_fn(name, move || h.upgrade().is_some_and(|h| probe(&h))); + } + } + for (what, probe) in [ + ("state", (|x: &Crossbar| x.st.is_locked()) as fn(&Crossbar) -> bool), + ("irq_order", |x| x.order.is_locked()), + ] { + let x = Arc::downgrade(&self.xbow); + register_lock_fn(format!("xbridge::crossbar::{what}"), move || { + x.upgrade().is_some_and(|x| probe(&x)) + }); + } + } + fn pci_head(&self, widget: u8) -> &Arc { + assert!((HEAD_E..=HEAD_F).contains(&widget)); + &self.heads[(widget - HEAD_E) as usize] + } + pub fn head(&self, widget: u8) -> Arc { + self.pci_head(widget).clone() + } + pub fn crossbar(&self) -> Arc { + self.xbow.clone() + } + /// A reader-specific widget-0 view; the host reads port 10 by default. + pub fn crossbar_for_port(&self, port: u8) -> Arc { + assert!((8..=15).contains(&port)); + Arc::new(CrossbarView { + xbow: self.xbow.clone(), + port, + }) + } + /// Host-port processor projection uses the XBridge widget-0 alias. + pub fn widget(&self, widget: u8) -> Option> { + match widget { + 0 | HOST_WIDGET => Some(self.crossbar()), + HEAD_E | HEAD_F => Some(self.head(widget)), + _ => self + .routes + .targets + .read() + .get(widget as usize) + .and_then(Clone::clone), + } + } + pub fn attach(&self, widget: u8, slot: u8, device: Arc) { + self.pci_head(widget).attach(slot, device); + } + pub fn dma(&self, widget: u8, slot: u8) -> Arc { + assert!(slot < 8); + Arc::new(SlotDma { + head: Arc::downgrade(self.pci_head(widget)), + slot, + }) + } + pub fn pin_line(&self, widget: u8, pin: u8) -> IrqLine { + assert!(pin < 8); + IrqLine::new( + Arc::new(Pin { + head: Arc::downgrade(self.pci_head(widget)), + pci: false, + }), + pin as u32, + ) + } + /// Raw PCI INTx is board wiring, independent of the eight head pins. + pub fn set_pci_irq(&self, widget: u8, slot: u8, pin: u8, output: IrqLine) { + assert!(slot < 8 && pin < 4); + self.pci_head(widget).pci_outputs.write()[(slot * 4 + pin) as usize] = Some(output); + } + pub fn set_interrupt_sender(&self, send: InterruptSender) { + *self.routes.interrupts.write() = Some(send); + } + pub fn set_dma_sender(&self, send: DmaSender) { + *self.routes.dma.write() = Some(send); + } + pub fn set_dma_64bit(&self, widget: u8, slot: u8, wide: bool) { + assert!(slot < 8); + let head = self.pci_head(widget); + { + let mut st = head.st.lock(); + st.dma_64bit = (st.dma_64bit & !(1 << slot)) | ((wide as u8) << slot); + } + head.publish_bars(); + } + pub fn set_wide_slot_mask(&self, widget: u8, mask: u8) { + self.pci_head(widget).st.lock().wide_slots = mask; + } + pub fn set_microlan(&self, widget: u8, bus: w1::Bus) { + *self.pci_head(widget).microlan.lock() = Some(bus); + } + /// An interrupt packet from the widget attached at `p.source_widget`, + /// routed as the heads' packets are. False when nothing takes it. + pub fn send_interrupt(&self, p: InterruptPacket) -> bool { + assert!((8..14).contains(&p.source_widget) && p.source_widget != HOST_WIDGET); + self.routes.interrupt(p) + } + pub fn attach_widget(&self, widget: u8, target: Arc) { + assert!((8..14).contains(&widget) && widget != HOST_WIDGET); + self.routes.targets.write()[widget as usize] = Some(target); + self.set_link_present(widget, true); + } + pub fn set_link_present(&self, widget: u8, present: bool) { + assert!((8..=15).contains(&widget)); + let mut st = self.xbow.st.lock(); + st.present = (st.present & !(1 << (widget - 8))) | ((present as u8) << (widget - 8)); + } + pub fn set_link_8bit(&self, widget: u8, enabled: bool) { + assert!((8..=15).contains(&widget)); + let mut st = self.xbow.st.lock(); + let bit = 1 << (widget - 8); + st.eight_bit = (st.eight_bit & !bit) | if enabled { bit } else { 0 }; + } + /// A pulse for each reset port, outside chip locks. A head's registers, + /// routed PCI lines and function headers are already reset. The board + /// must reset that port's attached engines through this callback. + pub fn set_reset_output(&self, output: Arc) { + *self.xbow.reset_output.write() = Some(output); + } + /// Time is supplied by the machine's virtual clock, in nanoseconds. + pub fn warm_reset(&self, source_port: u8, now_ns: u64) { + self.xbow.warm_reset(source_port, now_ns); + } + pub fn reset(&self) { + for h in &self.heads { + h.reset(); + } + let mut st = self.xbow.st.lock(); + st.reset(); + st.quiet_until = 0; + } +} +impl Resettable for XBridge { + fn power_on(&self) { + self.reset(); + } +} + +struct HeadState { + reg: [u32; 0x300 / 8], + ate: [u64; ATE_ENTRIES], + pins: u8, + dma_64bit: u8, + wide_slots: u8, +} +impl HeadState { + fn r(&self, off: usize) -> u32 { + self.reg[off / 8] + } + fn set(&mut self, off: usize, value: u32) { + self.reg[off / 8] = value; + } + fn reset(&mut self, speed: u8) { + self.reg.fill(0); + self.ate.fill(0); + self.pins = 0; + for (off, value) in [ + (0, 0x2d002049), + (8, 0x20), + (0x20, 0x0f00320f | ((speed as u32) << 4)), + (0x28, 0xfffff), + (reg::LINK_CONFIG, 0x03ff1810), + (0x80, 0x20000), + (0xa0, 0x20008), + (0xc0, 0x10000), + ] { + self.set(off, value); + } + for slot in 0..8 { + self.set( + 0x200 + slot * 8, + 0x18001000 + | if slot < 2 { + slot as u32 * 2 + } else { + slot as u32 + 2 + }, + ); + } + } + fn packet(&self, pin: Option, level: bool) -> InterruptPacket { + let upper = self.r(reg::INT_DEST_UPPER); + let mut address = ((upper as u64 & 0xffff) << 32) | self.r(reg::INT_DEST_LOWER) as u64; + let vector = if let Some(pin) = pin { + let host = self.r(reg::INTERRUPT_HOST_BASE + pin * 8); + address = (address & !(0x3ff << 38)) | (((host >> 8) as u64 & 0x3ff) << 38); + host as u8 + } else { + self.r(reg::INTERRUPT_ERROR_HOST) as u8 + }; + InterruptPacket { + source_widget: (self.r(reg::CONTROL) & 15) as u8, + widget: ((upper >> 16) & 15) as u8, + address, + vector, + level, + } + } +} +// Lock order: order, then microlan (NIC only), then st. +struct XBridgeHead { + widget: u8, + speed: u8, + st: Mutex, + order: Mutex<()>, + routes: Arc, + slots: RwLock<[Option>; 8]>, + pci_outputs: RwLock<[Option; 32]>, + microlan: Mutex>, + pio: Arc, + pio_map: Mutex, +} + +#[derive(Default)] +struct PioMap { + entries: HashMap<(u8, usize, usize), MappedBar>, + runs: Vec<(u64, u64)>, +} + +struct MappedBar { + function: Arc, + lanes: Arc, + device: Arc, +} + +/// Whole BAR-page runs are few and carry no RAM or native fault-handler state. +/// Readers clone one endpoint and release the map lock before calling a device. +#[derive(Default)] +struct PioDecode { + runs: RwLock>, +} +impl PioDecode { + fn publish(&self, mut runs: Vec) -> Vec { + runs.sort_unstable_by_key(|run| run.start); + debug_assert!(runs.windows(2).all(|pair| pair[0].end <= pair[1].start)); + std::mem::replace(&mut *self.runs.write(), runs) + } + fn target(&self, address: u64, size: usize) -> Option> { + let runs = self.runs.read(); + let i = runs.partition_point(|run| run.start <= address).checked_sub(1)?; + let run = &runs[i]; + (address.checked_add(size as u64)? <= run.end).then(|| run.dev.clone()) + } +} + +struct Lanes { + // BAR-relative CPU base, data-phase width and stream/swap policy. + policy: AtomicU64, + alias: bool, + size: u64, +} + +const PIO_INVALID: u64 = 1 << 63; + +impl Lanes { + fn access(&self, f: &dyn PciBusDevice, bar: usize, off: u32, size: usize, value: Option) -> u64 { + let policy = self.policy.load(Ordering::Acquire); + if policy & PIO_INVALID != 0 { + return if value.is_none() { u64::MAX >> (64 - size * 8) } else { 0 }; + } + self.cycle(f, bar, off.wrapping_sub(policy as u32), size, value, policy) + } + + fn cycle(&self, f: &dyn PciBusDevice, bar: usize, off: u32, size: usize, value: Option, policy: u64) -> u64 { + let width = if policy & (1 << 32) != 0 { 8 } else { 4 }; + if size > width { + let hi = self.cycle(f, bar, off, 4, value.map(|v| v >> 32), policy); + let lo = self.cycle(f, bar, off + 4, 4, value.map(|v| v as u32 as u64), policy); + return (hi << 32) | lo; + } + let selected = policy & (1 << 33) != 0; + let reverse = if self.alias { !selected } else { selected }; + let placed = if selected { off } else { off ^ (width - size) as u32 }; + // A reader may have obtained the old table entry before relocation. + // Its new policy must not send the stale address outside the BAR. + if placed as u64 + size as u64 > self.size { + return if value.is_none() { u64::MAX >> (64 - size * 8) } else { 0 }; + } + let mut bytes = [0; 8]; + if let Some(v) = value { + for (i, b) in bytes[..size].iter_mut().enumerate() { + let shift = if self.alias { (size - 1 - i) * 8 } else { i * 8 }; + *b = (v >> shift) as u8; + } + if reverse { bytes[..size].reverse(); } + } + XBridgeHead::bar_cycle(f, bar, placed, &mut bytes[..size], value.is_some()); + if reverse && value.is_none() { bytes[..size].reverse(); } + if self.alias { + bytes[..size].iter().fold(0, |v, &b| (v << 8) | b as u64) + } else { + bytes[..size].iter().enumerate().fold(0, |v, (i, &b)| v | ((b as u64) << (i * 8))) + } + } +} + +impl PciLanes for Lanes { + fn read(&self, f: &dyn PciBusDevice, bar: usize, off: u32, size: usize) -> u64 { + self.access(f, bar, off, size, None) + } + fn write(&self, f: &dyn PciBusDevice, bar: usize, off: u32, size: usize, value: u64) { + self.access(f, bar, off, size, Some(value)); + } +} +struct Pin { + head: Weak, + pci: bool, +} +impl InterruptController for Pin { + fn set_line(&self, line: u32, level: bool) { + if let Some(head) = self.head.upgrade() { + if self.pci { + let output = head + .pci_outputs + .read() + .get(line as usize) + .and_then(Clone::clone); + if let Some(output) = output { + output.set(level); + } else if level { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "xbridge: head {} PCI line {line} has no board wiring", + head.widget + ); + } + } else { + head.pin(line as usize, level); + } + } + } +} +struct SlotDma { + head: Weak, + slot: u8, +} +impl PciDma for SlotDma { + fn dma_read(&self, address: u64, bytes: &mut [u8]) -> PciDmaStatus { + if let Some(h) = self.head.upgrade() { + h.dma(self.slot, address, bytes, false) + } else { + bytes.fill(0); + PciDmaStatus::MasterAbort + } + } + fn dma_write(&self, address: u64, bytes: &[u8]) -> PciDmaStatus { + self.head.upgrade().map_or(PciDmaStatus::MasterAbort, |h| { + h.dma(self.slot, address, &mut bytes.to_vec(), true) + }) + } +} +impl XBridgeHead { + fn new(widget: u8, speed: u8, routes: Arc) -> Arc { + let mut st = HeadState { + reg: [0; 96], + ate: [0; ATE_ENTRIES], + pins: 0, + dma_64bit: 0, + wide_slots: 0, + }; + st.reset(speed); + Arc::new(Self { + widget, + speed, + st: Mutex::new(st), + order: Mutex::new(()), + routes, + slots: RwLock::new(std::array::from_fn(|_| None)), + pci_outputs: RwLock::new(std::array::from_fn(|_| None)), + microlan: Mutex::new(None), + pio: Arc::new(PioDecode::default()), + pio_map: Mutex::new(PioMap::default()), + }) + } + fn attach(self: &Arc, slot: u8, device: Arc) { + assert!(slot < 8); + self.slots.write()[slot as usize] = Some(device.clone()); + device.connect_dma(Arc::new(SlotDma { + head: Arc::downgrade(self), + slot, + })); + let pin = device.config().interrupt_pin(); + if (1..=4).contains(&pin) { + device.connect_interrupt(IrqLine::new( + Arc::new(Pin { + head: Arc::downgrade(self), + pci: true, + }), + slot as u32 * 4 + pin as u32 - 1, + )); + } + self.publish_bars(); + } + fn pin(&self, pin: usize, level: bool) { + if pin >= 8 { + return; + } + let _order = self.order.lock(); + let packet = { + let mut st = self.st.lock(); + let bit = 1 << pin; + let old = st.pins & bit != 0; + st.pins = (st.pins & !bit) | if level { bit } else { 0 }; + let pins = st.pins as u32; + let status = (st.r(reg::INTERRUPT_STATUS) & !255) | pins; + st.set(reg::INTERRUPT_STATUS, status); + (old != level + && st.r(reg::INTERRUPT_ENABLE) & bit as u32 != 0 + && (level || st.r(reg::INTERRUPT_MODE) & bit as u32 != 0)) + .then(|| st.packet(Some(pin), level)) + }; + if let Some(packet) = packet { + self.routes.interrupt(packet); + } + } + fn reset(&self) { + { + let _order = self.order.lock(); + self.st.lock().reset(self.speed); + } + let outputs = self.pci_outputs.read().clone(); + for line in outputs.into_iter().flatten() { + line.set(false); + } + let devices = self.slots.read().clone(); + for d in devices.into_iter().flatten() { + d.config().reset(); + } + self.publish_bars(); + } + fn invalid(&self, address: u64) { + let _order = self.order.lock(); + let packet = { + let mut st = self.st.lock(); + let first = st.r(reg::INTERRUPT_STATUS) & (1 << 24) == 0; + let status = st.r(reg::INTERRUPT_STATUS) | (1 << 24); + st.set(reg::INTERRUPT_STATUS, status); + let multi = st.r(reg::INTERRUPT_MULTIPLE) | (1 << 24); + st.set(reg::INTERRUPT_MULTIPLE, multi); + if first { + st.set(reg::ERR_UPPER, (address >> 32) as u32); + st.set(reg::ERR_LOWER, address as u32); + } + (first && st.r(reg::INTERRUPT_ENABLE) & (1 << 24) != 0).then(|| st.packet(None, true)) + }; + if let Some(p) = packet { + self.routes.interrupt(p); + } + crate::dlog!(crate::devlog::LogModule::Bedrock, + "xbridge: head {} invalid access at {address:#x}", + self.widget + ); + } + fn defined(off: usize) -> bool { + matches!(off, reg::IDENTIFICATION..=reg::TEST_PIN_CONTROL | reg::DIRECT_MAP | reg::SSRAM_PARITY_ERR | reg::ARBITRATION | reg::NIC | reg::BUS_TIMEOUT..=reg::PCI_ERR_LOWER | reg::INTERRUPT_STATUS..=reg::FORCE_PIN_LAST | reg::DEVICE_BASE..=reg::RRB_CLEAR) + } + fn deferred(address: u64) -> bool { + (0x300..0x600).contains(&address) + || (0x30000..0x30008).contains(&address) + || (0xc00000..0x1000000).contains(&address) + } + fn config(&self, address: u64, size: usize, value: Option) -> u64 { + if size == 8 { + let hi = self.config(address, 4, value.map(|v| v >> 32)); + return (hi << 32) | self.config(address + 4, 4, value.map(|v| v & 0xffffffff)); + } + let reg = ((address & 255) as usize) ^ (4 - size); + let slot = if address < 0x28000 { + ((address - 0x20000) >> 12) as usize + } else { + ((self.st.lock().r(reg::PCI_TYPE1_CONFIG) >> 11) & 31) as usize + }; + let bus = if address < 0x28000 { + 0 + } else { + (self.st.lock().r(reg::PCI_TYPE1_CONFIG) >> 16) & 255 + }; + let device = if bus == 0 && address & 0x700 == 0 { + self.slots.read().get(slot).and_then(Clone::clone) + } else { + None + }; + let mask = u32::MAX >> (32 - size * 8); + let shift = (reg & 3) * 8; + if let Some(dev) = device { + if let Some(value) = value { + dev.config().write_masked( + reg / 4, + (value as u32 & mask) << shift, + mask << shift, + ); + self.publish_bars(); + 0 + } else { + ((dev.config().read(reg / 4) >> shift) & mask) as u64 + } + } else { + if value.is_none() { + let mut st = self.st.lock(); + let multi = st.r(reg::INTERRUPT_MULTIPLE) | (1 << 11); + st.set(reg::INTERRUPT_MULTIPLE, multi); + } + mask as u64 + } + } + fn reg_write(&self, address: u64, value: u32) { + let off = address as usize & !7; + let _order = self.order.lock(); + let mut packets = Vec::new(); + if off == 0xb0 { + let pulse = (value >> 10) & 0x3ff; + let sample = (value >> 2) & 255; + let mut bus = self.microlan.lock(); + if let Some(bus) = bus.as_mut() { + let level = w1::slot::slot(bus, pulse, sample); + self.st.lock().set(reg::NIC, 2 | level as u32); + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "xbridge: head {} NIC has no board", self.widget); + } + return; + } + { + let mut st = self.st.lock(); + let masked = match off { + reg::CONTROL => (value & 0x0fe1f20f) | ((self.speed as u32) << 4), + reg::REQUEST_TIMEOUT | reg::INT_DEST_UPPER => value & 0xfffff, + reg::INT_DEST_LOWER | reg::RRB_EVEN | reg::RRB_ODD => value, + reg::LINK_CONFIG => value & 0x03ffffff, + reg::TEST_PIN_CONTROL => value & 0xffffff, + reg::DIRECT_MAP => value & 0xf3ffff, + reg::ARBITRATION => value & 0x3ff7f, + reg::BUS_TIMEOUT => value & 0x1f03ff, + reg::PCI_TYPE1_CONFIG => value & 0xfff800, + reg::INTERRUPT_DEVICE => value & 0xffffff, + reg::INTERRUPT_ERROR_HOST | reg::INTERRUPT_MODE => value & 255, + reg::INTERRUPT_ENABLE => { + let value = value & 0x7fffffff; + let newly = value & !st.r(reg::INTERRUPT_ENABLE) & st.pins as u32; + st.set(off, value); + for pin in 0..8 { + if newly & (1 << pin) != 0 { + packets.push(st.packet(Some(pin), true)); + } + } + value + } + reg::INTERRUPT_RESET => { + let clear = (if value & 2 != 0 { 1 << 30 } else { 0 }) + | (if value & 8 != 0 { 1 << 24 } else { 0 }); + let status = st.r(reg::INTERRUPT_STATUS) & !clear; + st.set(reg::INTERRUPT_STATUS, status); + let multi = if value & 64 != 0 { + 0 + } else { + st.r(reg::INTERRUPT_MULTIPLE) & !clear + }; + st.set(reg::INTERRUPT_MULTIPLE, multi); + return; + } + reg::FORCE_INTERRUPT_BASE..=reg::FORCE_PIN_LAST => { + let pin = (off - reg::FORCE_INTERRUPT_BASE) / 8 & 7; + if st.r(reg::INTERRUPT_ENABLE) & (1 << pin) == 0 { + crate::dlog!(crate::devlog::LogModule::Bedrock, "xbridge: masked force semantics unresolved"); + } else if off < reg::FORCE_PIN_BASE || st.pins & (1 << pin) != 0 { + packets.push(st.packet(Some(pin), true)); + } + 0 + } + reg::INTERRUPT_HOST_BASE..=reg::INTERRUPT_HOST_LAST => value & 0x3ffff, + reg::DEVICE_BASE..=reg::DEVICE_LAST => value & 0x1eefffff, + _ => { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "xbridge: head {} ignored register write {off:#x}", + self.widget + ); + return; + } + }; + if !(reg::FORCE_INTERRUPT_BASE..=reg::FORCE_PIN_LAST).contains(&off) { + st.set(off, masked); + } + } + for p in packets { + self.routes.interrupt(p); + } + drop(_order); + if off == reg::CONTROL || (reg::DEVICE_BASE..=reg::DEVICE_LAST).contains(&off) { + self.publish_bars(); + } + } + + fn publish_bars(&self) { + // Keep endpoint identities stable across BAR relocation. Publish the + // complete small decoder after dropping state and attachment locks. + let mut map = self.pio_map.lock(); + let (regs, control, wide) = { + let st = self.st.lock(); + let regs: [u32; 8] = std::array::from_fn(|i| st.r(reg::DEVICE_BASE + i * 8)); + (regs, st.r(reg::CONTROL), st.dma_64bit) + }; + let funcs: Vec<_> = self.slots.read().iter().enumerate() + .filter_map(|(slot, f)| Some((slot, f.clone()?))).collect(); + let windows: Vec<_> = funcs.iter().flat_map(|(slot, f)| { + f.config().windows().into_iter().map(move |(space, bar, base, size)| (*slot, bar, space, base as u64, base as u64 + size as u64)) + }).collect(); + map.runs.clear(); + let mut decoded = Vec::new(); + let mut retired = Vec::new(); + // Host replacement changes an endpoint's function. Guest configuration + // writes only update policy and never allocate another table endpoint. + map.entries.retain(|&(_, slot, _), entry| { + let keep = funcs.iter().any(|(s, f)| *s == slot && Arc::ptr_eq(f, &entry.function)); + if !keep { + entry.lanes.policy.fetch_or(PIO_INVALID, Ordering::AcqRel); + retired.push(entry.device.clone()); + } + keep + }); + for kind in 0u8..11 { + let (cpu, len, space, pci, width, selected) = if kind < 8 { + let slot = kind as usize; + let dev = regs[slot]; + let memory = dev & (1 << 12) != 0; + let mut base = dev & if memory { 0x3ff } else { 0xfff }; + if slot < 2 { base &= !1; } + let (cpu, len) = if slot < 2 { + (0x200000 + slot as u64 * 0x200000, 0x200000) + } else { + (0x600000 + (slot as u64 - 2) * 0x100000, 0x100000) + }; + (cpu, len, if memory { Space::Mem } else { Space::Io }, base << 20, + if dev & (1 << 22) != 0 { 8 } else { 4 }, dev & (1 << 13) != 0) + } else { + let (cpu, len, space, width) = match kind { + 8 => (0x40000000, 0x40000000, Space::Mem, 4), + 9 => (0x80000000, 0x40000000, Space::Mem, 8), + _ => (0x100000000, 0x100000000, Space::Io, 4), + }; + (cpu, len, space, 0, width, control & (1 << if space == Space::Mem { 22 } else { 23 }) != 0) + }; + let runs = bar_runs(&funcs, space, cpu, pci, len, |b| { + let width = if kind == 9 && wide & (1 << b.slot) == 0 { 4 } else { width }; + let policy = b.cpu_base as u32 as u64 | ((width == 8) as u64) << 32 | (selected as u64) << 33; + let entry = map.entries.entry((kind, b.slot, b.bar)).or_insert_with(|| { + let size = windows.iter().find(|&&(slot, bar, _, _, _)| slot == b.slot && bar == b.bar) + .map(|&(_, _, _, lo, hi)| hi - lo).unwrap(); + let lanes = Arc::new(Lanes { policy: AtomicU64::new(policy), alias: kind >= 8, size }); + let device = Arc::new(PciBar::new(lanes.clone(), b.dev.clone(), b.bar, u32::MAX as u64, 0)); + MappedBar { function: b.dev.clone(), lanes, device } + }); + entry.lanes.policy.store(policy, Ordering::Release); + entry.device.clone() + }); + for run in runs { + let lo = run.start - cpu + pci as u64; + let hi = run.end - cpu + pci as u64; + // bar_runs rounds to whole pages. A subpage BAR can still + // overlap one of those pages, so retain the slow decoder there. + let claims = windows.iter().filter(|&&(_, _, s, l, h)| s == space && l < hi && lo < h).count(); + if claims != 1 { continue; } + map.runs.push((run.start, run.end)); + decoded.push(run); + } + } + let previous = self.pio.publish(decoded); + drop(map); + // Replaced functions may run shutdown callbacks from their destructor. + drop(previous); + drop(retired); + } + + fn bar_cycle(dev: &dyn PciBusDevice, bar: usize, off: u32, bytes: &mut [u8], write: bool) { + for (i, part) in bytes.chunks_mut(4).enumerate() { + if write { + let v = part.iter().enumerate().fold(0, |v, (j, &b)| v | ((b as u32) << (j * 8))); + dev.bar_write(bar, off + i as u32 * 4, v, part.len()); + } else { + let v = dev.bar_read(bar, off + i as u32 * 4, part.len()); + for (j, b) in part.iter_mut().enumerate() { *b = (v >> (j * 8)) as u8; } + } + } + } + /// PCI functions see little-endian values; bytes here remain in address order. + fn pci_cycle(&self, space: Space, address: u32, bytes: &mut [u8], write: bool) -> bool { + let claim = self.slots.read().iter().flatten().find_map(|dev| { + let (bar, off) = dev.config().decode(space, address)?; + let (end_bar, end) = dev + .config() + .decode(space, address.checked_add(bytes.len() as u32 - 1)?)?; + (end_bar == bar && end == off + bytes.len() as u32 - 1).then(|| (dev.clone(), bar, off)) + }); + let Some((dev, bar, off)) = claim else { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "xbridge: head {} no PCI BAR for {space:?} {address:#x}", + self.widget + ); + if !write { + bytes.fill(0xff); + } + return false; + }; + Self::bar_cycle(&*dev, bar, off, bytes, write); + true + } + fn window_cycle(&self, slot: usize, off: u32, bytes: &mut [u8], write: bool) { + let dev = self.st.lock().r(reg::DEVICE_BASE + slot * 8); + let width = if dev & (1 << 22) != 0 { 8 } else { 4 }; + if bytes.len() > width { + // Device windows split a doubleword high word first. + let (low, high) = bytes.split_at_mut(4); + self.window_cycle(slot, off, high, write); + self.window_cycle(slot, off + 4, low, write); + return; + } + let memory = dev & (1 << 12) != 0; + let mut base = dev & if memory { 0x3ff } else { 0xfff }; + if slot < 2 { + base &= !1; + } + let stream = dev & (1 << 13) != 0; + let placed = if stream { + off + } else { + off ^ (width - bytes.len()) as u32 + }; + if stream && write { + bytes.reverse(); + } + self.pci_cycle( + if memory { Space::Mem } else { Space::Io }, + (base << 20).wrapping_add(placed), + bytes, + write, + ); + if stream && !write { + bytes.reverse(); + } + } + fn alias_cycle(&self, space: Space, address: u32, width: usize, bytes: &mut [u8], write: bool) { + let width = if width == 8 { + let slots = self.slots.read().clone(); + let access_len = bytes.len(); + let matches: Vec<_> = slots + .iter() + .enumerate() + .flat_map(|(slot, dev)| { + dev.as_ref() + .map(|d| d.config().windows()) + .unwrap_or_default() + .into_iter() + .filter_map(move |(s, _, base, size)| { + (s == space + && address >= base + && (address as u64 + access_len as u64) + <= base as u64 + size as u64) + .then_some(slot) + }) + }) + .collect(); + if matches.len() == 1 && self.st.lock().dma_64bit & (1 << matches[0]) == 0 { + 4 + } else { + 8 + } + } else { + width + }; + if bytes.len() > width { + let (first, last) = bytes.split_at_mut(width); + self.alias_cycle(space, address, width, first, write); + self.alias_cycle(space, address + width as u32, width, last, write); + return; + } + let swap = self.st.lock().r(reg::CONTROL) & (1 << if space == Space::Mem { 22 } else { 23 }) != 0; + let address = if swap { + address + } else { + address ^ (width - bytes.len()) as u32 + }; + if !swap && write { + bytes.reverse(); + } + self.pci_cycle(space, address, bytes, write); + if !swap && !write { + bytes.reverse(); + } + } + fn slow_pio(&self, address: u64, size: usize, value: Option) -> u64 { + let write = value.is_some(); + let mut bytes = [0u8; 8]; + if (0x200000..0xc00000).contains(&address) { + let (slot, off) = if address < 0x600000 { + (((address - 0x200000) >> 21) as usize, address & 0x1fffff) + } else { + (2 + ((address - 0x600000) >> 20) as usize, address & 0xfffff) + }; + if let Some(v) = value { + for (i, b) in bytes[..size].iter_mut().enumerate() { + *b = (v >> (i * 8)) as u8; + } + } + self.window_cycle(slot, off as u32, &mut bytes[..size], write); + return bytes[..size] + .iter() + .enumerate() + .fold(0, |v, (i, &b)| v | ((b as u64) << (i * 8))); + } + let alias = if (0x40000000..0x80000000).contains(&address) { + Some((Space::Mem, address - 0x40000000, 4)) + } else if (0x80000000..0xc0000000).contains(&address) { + Some((Space::Mem, address - 0x80000000, 8)) + } else if (0x100000000..0x200000000).contains(&address) { + Some((Space::Io, address - 0x100000000, 4)) + } else { + None + }; + if let Some((space, off, width)) = alias { + if let Some(v) = value { + for (i, b) in bytes[..size].iter_mut().enumerate() { + *b = (v >> ((size - 1 - i) * 8)) as u8; + } + } + self.alias_cycle(space, off as u32, width, &mut bytes[..size], write); + return bytes[..size].iter().fold(0, |v, &b| (v << 8) | b as u64); + } + 0 + } + fn access(&self, address: u64, size: usize, value: Option) -> u64 { + let write = value.is_some(); + if (0x20000..0x29000).contains(&address) { + return self.config(address, size, value); + } + if (0x200000..0xc00000).contains(&address) + || (0x40000000..0xc0000000).contains(&address) + || (0x100000000..0x200000000).contains(&address) + { + // An unaligned access can straddle a BAR's final mapped page. + if address & 0xfff > 0x1000 - size as u64 { + return self.slow_pio(address, size, value); + } + let Some(endpoint) = self.pio.target(address, size) else { + return self.slow_pio(address, size, value); + }; + return if let Some(v) = value { + match size { + 1 => { endpoint.write8(address, v as u8); } + 2 => { endpoint.write16(address, v as u16); } + 4 => { endpoint.write32(address, v as u32); } + _ => { endpoint.write64(address, v); } + } + 0 + } else { + match size { + 1 => endpoint.read8(address).data as u64, + 2 => endpoint.read16(address).data as u64, + 4 => endpoint.read32(address).data as u64, + _ => endpoint.read64(address).data, + } + }; + } + if size == 8 && address & 7 == 0 && (reg::ATE_UPPER_BASE..reg::ATE_LOWER_BASE).contains(&address) && write { + self.st.lock().ate[((address - reg::ATE_UPPER_BASE) / 8) as usize] = + value.unwrap() & 0x0000ffffffffffff; + return 0; + } + if size == 4 && address & 7 == 4 { + if !write { + if (reg::ATE_UPPER_BASE..reg::ATE_LOWER_BASE).contains(&address) { + return self.st.lock().ate[((address - reg::ATE_UPPER_BASE) / 8) as usize] >> 32; + } + if (reg::ATE_LOWER_BASE..reg::ATE_END).contains(&address) { + return self.st.lock().ate[((address - reg::ATE_LOWER_BASE) / 8) as usize] as u32 as u64; + } + } + if address < 0x300 && Self::defined(address as usize & !7) { + if let Some(value) = value { + self.reg_write(address, value as u32); + return 0; + } + return self.st.lock().r(address as usize & !7) as u64; + } + } + if Self::deferred(address) { + crate::dlog!(crate::devlog::LogModule::Bedrock, + "xbridge: head {} deferred access at {address:#x}", + self.widget + ); + } else { + self.invalid(address); + } + 0 + } + fn translate( + &self, + slot: u8, + address: u64, + write: bool, + packets: bool, + ) -> Option<(u8, u64, u64, bool)> { + let mut st = self.st.lock(); + let dev = st.r(reg::DEVICE_BASE + slot as usize * 8); + let wide = st.dma_64bit & (1 << slot) != 0; + let device_swap = dev & (1 << 19) != 0; + let (widget, target, swap, direct, page, wide64) = if (0x40000000..0x40800000) + .contains(&address) + || (0x60000000..0x60800000).contains(&address) + { + let shift = if st.r(reg::CONTROL) & (1 << 21) != 0 { 14 } else { 12 }; + let index = ((address & 0x1fffffff) >> shift) as usize; + if index >= ATE_MAPPED || st.ate[index] & 1 == 0 { + let v = st.r(reg::INTERRUPT_STATUS) | (1 << 30); + st.set(reg::INTERRUPT_STATUS, v); + let v = st.r(reg::INTERRUPT_MULTIPLE) | (1 << 30); + st.set(reg::INTERRUPT_MULTIPLE, v); + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "xbridge: invalid page DMA {address:#x}, entry {index}" + ); + return None; + } + let ate = st.ate[index]; + let mask = (1 << shift) - 1; + ( + ((ate >> 8) & 15) as u8, + (ate & 0x0000fffffffff000 & !mask) | (address & mask), + ate & 32 != 0 || address & (1 << 29) != 0, + false, + true, + false, + ) + } else if address >= 0x1000000000000000 { + ( + (address >> 60) as u8, + address & 0x0000ffffffffffff, + address & (1 << 55) != 0, + false, + false, + true, + ) + } else if (0x80000000..=0xffffffff).contains(&address) { + let direct = st.r(reg::DIRECT_MAP); + let mut target = ((direct as u64 & 0x1ffff) << 31) | (address & 0x7fffffff); + if direct & 0x1ffff == 0 && direct & (1 << 17) != 0 { + target += 0x20000000; + } + ( + ((direct >> 20) & 15) as u8, + target, + device_swap, + true, + false, + false, + ) + } else { + crate::dlog!(crate::devlog::LogModule::Bedrock, "xbridge: unsupported DMA {address:#x}"); + return None; + }; + let half = packets + && swap + && st.wide_slots & (1 << slot) != 0 + && if write { + direct || page || (wide64 && device_swap) + } else { + direct || (wide64 && wide && device_swap) || (page && address & (1 << 29) != 0) + }; + let mask = if swap || half { + 0 + } else if wide { + 7 + } else { + 3 + }; + Some((widget, target, mask, half)) + } + // Retain the existing memory path's probe before issuing a chunk's writes. + // The neutral helper alone can commit bytes before reporting a refusal. + fn write_memory(mem: &dyn BusDevice, address: u64, data: &[u8]) -> bool { + if data.is_empty() { + return true; + } + let first = address & !7; + let Some(end) = address + .checked_add(data.len() as u64) + .and_then(|a| a.checked_add(7)) + else { + return false; + }; + let mut probe = vec![0; ((end & !7) - first) as usize]; + read_bytes(mem, first, &mut probe) == BUS_OK && write_bytes(mem, address, data) == BUS_OK + } + fn dma(&self, slot: u8, address: u64, bytes: &mut [u8], write: bool) -> PciDmaStatus { + let sender = self.routes.dma.read().clone(); + let mut good = true; + let mut done = 0; + while done < bytes.len() { + let Some(at) = address.checked_add(done as u64) else { + if !write { + bytes[done..].fill(0); + } + return PciDmaStatus::MasterAbort; + }; + let n = (0x1000 - (at as usize & 0xfff)).min(bytes.len() - done); + if (0x3fff0000..0x40000000).contains(&at) { + if !write { + bytes[done..done + n].fill(0); + } + done += n; + continue; + } + if !write { + bytes[done..done + n].fill(0); + } + let translated = self.translate(slot, at, write, sender.is_some()); + let accepted = if let Some((widget, target, mask, half)) = translated { + if let Some(send) = &sender { + // Packet legs use naturally aligned accesses, retaining source identity. + let mut offset = 0; + let mut ok = true; + while offset < n { + let p = target + offset as u64; + let mut size = 8usize; + while size > n - offset || p & (size as u64 - 1) != 0 { + size /= 2; + } + let mut data = [0u8; 8]; + if write { + for i in 0..size { + data[i] = bytes[done + + offset + + if half { + i + } else { + i ^ (mask as usize & (size - 1)) + }]; + } + } + let placed = if half { + p + } else { + p ^ (mask & !(size as u64 - 1)) + }; + let packet = DmaPacket { + source_widget: (self.st.lock().r(reg::CONTROL) & 15) as u8, + slot, + widget, + address: placed, + write, + half_write_requested: half && write && size == 8, + half_read_requested: half && !write && size == 8, + }; + let part = send(packet, &mut data[..size]); + ok &= part; + if !write { + for i in 0..size { + bytes[done + offset + i] = if part { + data[if half { + i + } else { + i ^ (mask as usize & (size - 1)) + }] + } else { + 0 + }; + } + } + offset += size; + } + ok + } else { + let target_device = self.routes.targets.read()[widget as usize].clone(); + target_device.is_some_and(|mem| { + let low = target & !mask; + let high = (target + n as u64 + mask) & !mask; + let mut data = vec![0; (high - low) as usize]; + if write { + for i in 0..n { + data[(((target + i as u64) ^ mask) - low) as usize] = + bytes[done + i]; + } + if low == target && high == target + n as u64 { + Self::write_memory(&*mem, low, &data) + } else { + let mut ok = true; + for i in 0..n { + let p = (target + i as u64) ^ mask; + ok &= Self::write_memory( + &*mem, + p, + &data[(p - low) as usize..(p - low) as usize + 1], + ); + } + ok + } + } else { + // Preserve the old helper's covering-doubleword probe + // and zero the whole failed physical chunk, even if + // the neutral helper returned part of its contents. + let first = low & !7; + let mut fetched = vec![0; (((high + 7) & !7) - first) as usize]; + let ok = read_bytes(&*mem, first, &mut fetched) == BUS_OK; + for i in 0..n { + bytes[done + i] = if ok { + fetched[(((target + i as u64) ^ mask) - first) as usize] + } else { + 0 + }; + } + ok + } + }) + } + } else { + false + }; + if !accepted { + good = false; + } + done += n; + } + if good { + PciDmaStatus::Ok + } else { + PciDmaStatus::MasterAbort + } + } +} + +struct XbowState { + reg: [u32; 96], + status: u32, + present: u8, + eight_bit: u8, + quiet_until: u64, +} +impl XbowState { + fn new() -> Self { + let mut st = Self { + reg: [0; 96], + status: 0, + present: (1 << 2) | (1 << 6) | (1 << 7), + eight_bit: 0, + quiet_until: 0, + }; + st.reset(); + st + } + fn reset(&mut self) { + self.reg.fill(0); + self.status = 0; + for (off, value) in [ + (xbow::IDENTIFICATION, 0x2d000001), + (xbow::CONTROL, 0x80), + (xbow::PACKET_TIMEOUT, 0xfffff), + (xbow::INT_UPPER, 0x80000), + (xbow::LLP_CONTROL, 0x03ff1810), + (xbow::ARB_RELOAD, 2), + (xbow::NIC, 2), + (xbow::FENCE_BASE, 255), + ] { + self.reg[ix(off)] = value; + } + for i in 0..8 { + self.reg[ix(xbow::FENCE_BASE + 8 + i * 8)] = 255; + self.reg[ix(xbow::LINK_BASE + xbow::LINK_CONTROL + i * xbow::LINK_STRIDE)] = 0x80000; + } + } + fn packet(&self, level: bool) -> InterruptPacket { + let upper = self.reg[ix(xbow::INT_UPPER)]; + InterruptPacket { + source_widget: 0, + widget: ((upper >> 16) & 15) as u8, + address: ((upper as u64 & 0xffff) << 32) | self.reg[ix(xbow::INT_LOWER)] as u64, + vector: (upper >> 24) as u8, + level, + } + } +} +// Lock order: order, then st. Reset outputs run with both released. +struct Crossbar { + st: Mutex, + order: Mutex<()>, + routes: Arc, + heads: [Weak; 2], + reset_output: RwLock>>, +} +impl Crossbar { + fn reset_port(&self, port: u8) { + if (14..=15).contains(&port) { + if let Some(h) = self.heads[(port - 14) as usize].upgrade() { + h.reset(); + } + } + let out = self.reset_output.read().clone(); + if let Some(out) = out { + out(port); + } + } + fn warm_reset(&self, source: u8, now: u64) { + assert!((8..=15).contains(&source)); + let ports = { + let mut st = self.st.lock(); + if now < st.quiet_until { + return; + } + st.quiet_until = now.saturating_add(10240); + if st.reg[ix(xbow::FENCE_BASE)] & (1 << (source - 8)) == 0 { + return; + } + let ports: Vec<_> = (0..8) + .filter(|&i| st.reg[ix(xbow::FENCE_BASE + 8 + i * 8)] & (1 << (source - 8)) != 0) + .map(|i| i as u8 + 8) + .collect(); + st.reset(); + ports + }; + for port in ports { + self.reset_port(port); + } + } + fn access(&self, port: u8, address: u64, size: usize, value: Option) -> u64 { + let _order = self.order.lock(); + let mut packet = None; + let mut reset = None; + let mut access_error = false; + let result = { + let mut st = self.st.lock(); + let off = address as u32 & !7; + if size != 4 || address & 7 != 4 || address >= xbow::LINK_END as u64 || (READ_ONLY_BASE..=READ_ONLY_LAST).contains(&off) { + st.status |= 32; + access_error = true; + if st.reg[ix(xbow::CONTROL)] & 32 != 0 && st.status & (1 << 23) == 0 { + st.status |= 1 << 23; + packet = Some(st.packet(true)); + } + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "xbridge-xbow: invalid access {address:#x} width {size}" + ); + 0 + } else if let Some(v) = value { + let v = v as u32; + if off >= xbow::LINK_BASE { + match (off - xbow::LINK_BASE) % xbow::LINK_STRIDE { + xbow::LINK_CONTROL => st.reg[ix(off)] = v & 0xbf7f01ff, + xbow::LINK_ARB_UPPER | xbow::LINK_ARB_LOWER => st.reg[ix(off)] = v, + xbow::LINK_RESET => reset = Some((8 + (off - xbow::LINK_BASE) / xbow::LINK_STRIDE) as u8), + _ => { + st.status |= 32; + access_error = true; + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "xbridge-xbow: read-only link register offset {off:#x}" + ); + } + } + } else { + match off { + xbow::CONTROL => st.reg[ix(off)] = v & 0xfe, + xbow::PACKET_TIMEOUT => st.reg[ix(off)] = v & 0xfffff, + xbow::INT_UPPER => st.reg[ix(off)] = (v & 0xff07ffff) | 0x80000, + xbow::INT_LOWER => st.reg[ix(off)] = v, + xbow::ERR_CMD => { + st.reg[ix(xbow::ERR_CMD)] = 0; + st.reg[ix(xbow::ERR_UPPER)] = 0; + st.reg[ix(xbow::ERR_LOWER)] = 0; + } + xbow::LLP_CONTROL => st.reg[ix(off)] = v & 0x3ffffff, + xbow::ARB_RELOAD => st.reg[ix(off)] = v & 0x3f, + xbow::PERF_A | xbow::PERF_B => st.reg[ix(off)] = v & 0x700000, + xbow::NIC => { + st.reg[ix(off)] = (v & 0xffffc) | 2; + if v & 0xffc00 != 0 { + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "xbridge-xbow: one-wire pulse unimplemented at register offset 0x70" + ); + } + } + xbow::FENCE_BASE..=FENCE_LAST => st.reg[ix(off)] = v & 255, + xbow::LOCK => { + if st.reg[ix(off)] == 0 { + st.reg[ix(off)] = v & 255; + } + } + xbow::LOCK_CLEAR => st.reg[ix(xbow::LOCK)] = 0, + _ => { + st.status |= 32; + access_error = true; + crate::dlog!( + crate::devlog::LogModule::Bedrock, + "xbridge-xbow: read-only register offset {off:#x}" + ); + } + } + } + 0 + } else if off >= xbow::LINK_BASE { + let link = (off - xbow::LINK_BASE) / xbow::LINK_STRIDE; + let present = st.present & (1 << link) != 0; + match (off - xbow::LINK_BASE) % xbow::LINK_STRIDE { + xbow::LINK_FLUSH => 0, + xbow::LINK_STATUS | xbow::LINK_STATUS_CLEAR => { + if present { + 1 << 31 + } else { + 1 << 5 + } + } + xbow::LINK_AUX_STATUS => { + (if present { 1 << 5 } else { 1 << 6 }) + | (if st.eight_bit & (1 << link) != 0 { + 1 << 4 + } else { + 0 + }) + } + xbow::LINK_RESET => { + st.status |= 32; + access_error = true; + 0 + } + _ => st.reg[ix(off)] as u64, + } + } else { + match off { + xbow::STATUS => (st.status | ((port as u32) << 6)) as u64, + xbow::STATUS_CLEAR => { + let v = st.status | ((port as u32) << 6); + if st.status & (1 << 23) != 0 { + packet = Some(st.packet(false)); + } + st.status &= !((1 << 23) | 0x3f); + v as u64 + } + xbow::LOCK_CLEAR => st.reg[ix(xbow::LOCK)] as u64, + _ => st.reg[ix(off)] as u64, + } + } + }; + if access_error && packet.is_none() { + let mut st = self.st.lock(); + if st.reg[ix(xbow::CONTROL)] & 32 != 0 && st.status & (1 << 23) == 0 { + st.status |= 1 << 23; + packet = Some(st.packet(true)); + } + } + if let Some(p) = packet { + self.routes.interrupt(p); + } + // Reset outputs may call back into the chip, so release ordering too. + drop(_order); + if let Some(port) = reset { + self.reset_port(port); + } + result + } +} +struct CrossbarView { + xbow: Arc, + port: u8, +} + +macro_rules! bus_view { + ($ty:ty, $read:expr) => { + impl BusDevice for $ty { + fn read8(&self, a: u64) -> BusRead8 { + BusRead8::ok(($read)(self, a, 1, None) as u8) + } + fn read16(&self, a: u64) -> BusRead16 { + BusRead16::ok(($read)(self, a, 2, None) as u16) + } + fn read32(&self, a: u64) -> BusRead32 { + BusRead32::ok(($read)(self, a, 4, None) as u32) + } + fn read64(&self, a: u64) -> BusRead64 { + BusRead64::ok(($read)(self, a, 8, None)) + } + fn write8(&self, a: u64, v: u8) -> u32 { + ($read)(self, a, 1, Some(v as u64)); + BUS_OK + } + fn write16(&self, a: u64, v: u16) -> u32 { + ($read)(self, a, 2, Some(v as u64)); + BUS_OK + } + fn write32(&self, a: u64, v: u32) -> u32 { + ($read)(self, a, 4, Some(v as u64)); + BUS_OK + } + fn write64(&self, a: u64, v: u64) -> u32 { + ($read)(self, a, 8, Some(v)); + BUS_OK + } + } + }; +} +bus_view!(XBridgeHead, |s: &XBridgeHead, a, n, v| s.access(a, n, v)); +bus_view!(Crossbar, |s: &Crossbar, a, n, v| s.access( + HOST_WIDGET, + a, + n, + v +)); +bus_view!(CrossbarView, |s: &CrossbarView, a, n, v| s + .xbow + .access(s.port, a, n, v)); + +#[cfg(test)] +mod tests; diff --git a/src/dev/xbridge/tests.rs b/src/dev/xbridge/tests.rs new file mode 100644 index 000000000..1caeff8ce --- /dev/null +++ b/src/dev/xbridge/tests.rs @@ -0,0 +1,738 @@ +// Cases follow tests/qtest/sn1-xbridge-test.c (page-dma, interrupt-packets, +// access-errors), sn1-xbridge-chip-test.c (head-dma-reaches-the-host, +// warm-reset-reaches-the-heads) and sn1-xbridge-xbow-test.c (reset-values, +// lock, fences, link-reset-one-shot) in jamesbraid/qemu-sgi-origami. +use super::*; +use crate::traits::BUS_ERR; +struct Memory(Mutex>); +impl BusDevice for Memory { + fn read32(&self, a: u64) -> BusRead32 { + let mem = self.0.lock(); + if a as usize + 4 > mem.len() { + return BusRead32::err(); + } + BusRead32::ok( + mem[a as usize..a as usize + 4] + .iter() + .fold(0, |v, &b| (v << 8) | b as u32), + ) + } + fn write32(&self, a: u64, v: u32) -> u32 { + let mut mem = self.0.lock(); + if a as usize + 4 > mem.len() { + return BUS_ERR; + } + for i in 0..4 { + mem[a as usize + i] = (v >> (24 - i * 8)) as u8; + } + BUS_OK + } + + fn read64(&self, a: u64) -> BusRead64 { + let mem = self.0.lock(); + if a as usize + 8 > mem.len() { + return BusRead64::err(); + } + BusRead64::ok( + mem[a as usize..a as usize + 8] + .iter() + .fold(0, |v, &b| (v << 8) | b as u64), + ) + } + fn write64(&self, a: u64, v: u64) -> u32 { + let mut mem = self.0.lock(); + if a as usize + 8 > mem.len() { + return BUS_ERR; + } + for i in 0..8 { + mem[a as usize + i] = (v >> (56 - i * 8)) as u8; + } + BUS_OK + } + fn write64_masked(&self, a: u64, v: u64, mask: u64) -> u32 { + let old = self.read64(a); + if old.status != BUS_OK { + return BUS_ERR; + } + self.write64(a, (old.data & !mask) | (v & mask)) + } + + fn read8(&self, a: u64) -> BusRead8 { + match self.0.lock().get(a as usize) { + Some(&value) => BusRead8 { + data: value, + status: BUS_OK, + }, + None => BusRead8 { + data: 0, + status: BUS_ERR, + }, + } + } + fn write8(&self, a: u64, v: u8) -> u32 { + match self.0.lock().get_mut(a as usize) { + Some(b) => { + *b = v; + BUS_OK + } + None => BUS_ERR, + } + } +} +fn chip() -> (Arc, Arc) { + let mem = Arc::new(Memory(Mutex::new(vec![0; 0x8000]))); + (XBridge::new(mem.clone()), mem) +} + +/// The machine lock registry must not extend the lifetime of a chip or attached function. +#[test] +fn lock_registry_does_not_keep_the_chip_or_its_devices_alive() { + let memory = Arc::new(Memory(Mutex::new(vec![0; 0x8000]))); + let memory_ref = Arc::downgrade(&memory); + let chip = XBridge::new(memory); + let chip_ref = Arc::downgrade(&chip); + let heads = chip.heads.each_ref().map(Arc::downgrade); + let pio = chip.heads.each_ref().map(|head| Arc::downgrade(&head.pio)); + let crossbar = Arc::downgrade(&chip.xbow); + let card = Card::with_bar(crate::pci::Bar::Mem(0x1000), 0); + let card_ref = Arc::downgrade(&card); + chip.attach(15, 1, card.clone()); + chip.set_reset_output(Arc::new(move |_| card.config.reset())); + chip.head(15).write32(0x21010, 0x02000000); + chip.head(15).write32(0x21004, 2); + assert!(!chip.pci_head(15).pio_map.lock().runs.is_empty()); + chip.register_locks(); + + assert!(heads.iter().all(|head| head.upgrade().is_some())); + assert!(crossbar.upgrade().is_some()); + assert!(card_ref.upgrade().is_some()); + assert!(memory_ref.upgrade().is_some()); + drop(chip); + assert!(chip_ref.upgrade().is_none()); + assert!(heads.iter().all(|head| head.upgrade().is_none())); + assert!(pio.iter().all(|view| view.upgrade().is_none())); + assert!(crossbar.upgrade().is_none()); + assert!(card_ref.upgrade().is_none()); + assert!(memory_ref.upgrade().is_none()); +} +/// A DMA packet reports failure without losing the valid prefix already transferred. +#[test] +fn page_dma_keeps_valid_prefix_and_refuses_invalid_suffix() { + physical_chunk_refusal_does_not_publish_a_prefix(); + let (c, mem) = chip(); + let f = c.head(15); + f.write64(reg::ATE_UPPER_BASE as u64, 0x2000 | (10 << 8) | 0x21); + let dma = c.dma(15, 2); + let bytes = [1, 2, 3, 4, 5, 6, 7, 8]; + assert_eq!(dma.dma_write(0x40000ffc, &bytes), PciDmaStatus::MasterAbort); + assert_eq!(&mem.0.lock()[0x2ffc..0x3000], &[1, 2, 3, 4]); + assert_eq!(&mem.0.lock()[0x3000..0x3004], &[0; 4]); + let mut read = [0xff; 8]; + assert_eq!( + dma.dma_read(0x40000ffc, &mut read), + PciDmaStatus::MasterAbort + ); + assert_eq!(read, [1, 2, 3, 4, 0, 0, 0, 0]); + assert_ne!(f.read32((reg::INTERRUPT_STATUS + 4) as u64).data & (1 << 30), 0); + f.write32((reg::INTERRUPT_RESET + 4) as u64, 2); + assert_eq!(f.read32((reg::INTERRUPT_STATUS + 4) as u64).data & (1 << 30), 0); + f.write64((reg::ATE_UPPER_BASE + 800 * 8) as u64, 0xffff123456789abc); + assert_eq!(f.read32((reg::ATE_UPPER_BASE + 4 + 800 * 8) as u64).data, 0x1234); + assert_eq!(f.read32((reg::ATE_LOWER_BASE + 4 + 800 * 8) as u64).data, 0x56789abc); + f.write64((reg::ATE_UPPER_BASE + 512 * 8) as u64, 0x2a21); + f.write32((reg::DIRECT_MAP + 4) as u64, 9 << 20); + for (address, status) in [(0x80002000, PciDmaStatus::MasterAbort), + (0x3fff8000, PciDmaStatus::Ok), (0x40800000, PciDmaStatus::MasterAbort), + (0x40200000, PciDmaStatus::MasterAbort)] { + let mut bytes = [0xff; 4]; + assert_eq!(dma.dma_read(address, &mut bytes), status); + assert_eq!(bytes, [0; 4]); + } +} +// A mapped memory range whose middle word refuses accesses. +struct FaultMemory(Arc); +impl BusDevice for FaultMemory { + fn read8(&self, a: u64) -> BusRead8 { + if (0x2004..0x2008).contains(&a) { + BusRead8::err() + } else { + self.0.read8(a) + } + } + fn write8(&self, a: u64, v: u8) -> u32 { + if (0x2004..0x2008).contains(&a) { + BUS_ERR + } else { + self.0.write8(a, v) + } + } + fn read32(&self, a: u64) -> BusRead32 { + if (0x2004..0x2008).contains(&a) { + BusRead32::err() + } else { + self.0.read32(a) + } + } + fn write32(&self, a: u64, v: u32) -> u32 { + if (0x2004..0x2008).contains(&a) { + BUS_ERR + } else { + self.0.write32(a, v) + } + } +} +fn physical_chunk_refusal_does_not_publish_a_prefix() { + let mem = Arc::new(Memory(Mutex::new(vec![0xaa; 0x8000]))); + let c = XBridge::new(Arc::new(FaultMemory(mem.clone()))); + let f = c.head(15); + f.write32((reg::DIRECT_MAP + 4) as u64, 10 << 20); + f.write32((reg::DEVICE_BASE + 4) as u64, 1 << 19); + let dma = c.dma(15, 0); + let mut bytes = [0xff; 8]; + assert_eq!( + dma.dma_read(0x80002002, &mut bytes), + PciDmaStatus::MasterAbort + ); + assert_eq!( + bytes, [0; 8], + "a failed physical chunk zeroes its readable prefix and suffix" + ); + assert_eq!( + dma.dma_write(0x80002002, &[1; 8]), + PciDmaStatus::MasterAbort + ); + assert_eq!( + &mem.0.lock()[0x2000..0x2010], + &[0xaa; 16], + "the probe prevents writes before or after the refused word" + ); +} +/// Enabling an asserted guest interrupt pin delivers its programmed destination. +#[test] +fn interrupt_keeps_destination_and_enabling_a_live_pin_asserts() { + let (c, _) = chip(); + let f = c.head(15); + let packets = Arc::new(Mutex::new(Vec::new())); + let capture = packets.clone(); + c.set_interrupt_sender(Arc::new(move |p| { + capture.lock().push(p); + true + })); + f.write32((reg::INT_DEST_UPPER + 4) as u64, 0xa0001); + f.write32((reg::INT_DEST_LOWER + 4) as u64, 0x23450080); + f.write32((reg::INTERRUPT_HOST_BASE + 4) as u64, (0x155 << 8) | 0x42); + c.pin_line(15, 0).set(true); + assert!(packets.lock().is_empty()); + f.write32((reg::INTERRUPT_ENABLE + 4) as u64, 1); + f.write32((reg::INTERRUPT_MODE + 4) as u64, 1); + c.pin_line(15, 0).set(false); + let p = packets.lock(); + assert_eq!(p.len(), 2); + assert_eq!(p[0].source_widget, 15); + assert_eq!(p[0].widget, 10); + assert_eq!(p[0].vector, 0x42); + assert_eq!(p[0].address, (0x123450080 & !(0x3ff << 38)) | (0x155 << 38)); + assert!(p[0].level); + assert!(!p[1].level); + drop(p); + let card = Card::new(); + c.attach(15, 2, card.clone()); + let (probe, line) = crate::irq::LevelProbe::line(); + c.set_pci_irq(15, 2, 0, line); + card.irq.lock().as_ref().unwrap().set(true); + assert!(probe.level()); + assert_eq!(f.read32((reg::INTERRUPT_STATUS + 4) as u64).data & 255, 0); + c.reset(); + assert!(!probe.level()); +} + +/// The guest receives host DMA packets with their original widget and address placement. +#[test] +fn host_dma_places_packets_and_rejects_invalid_addresses() { + let (c, mem) = chip(); + let f = c.head(15); + let d = c.dma(15, 0); + f.write32((reg::DIRECT_MAP + 4) as u64, 10 << 20); + f.write64(reg::ATE_UPPER_BASE as u64, 0x3000 | (10 << 8) | 1); + for (wide, address, target, expected) in [ + (false, 0x80002000, 0x2000, [3, 2, 1, 0, 7, 6, 5, 4]), + (true, 0x80002000, 0x2000, [7, 6, 5, 4, 3, 2, 1, 0]), + (true, 0x60000000, 0x3000, [0, 1, 2, 3, 4, 5, 6, 7]), + (true, (10 << 60) | (1 << 55) | 0x2000, 0x2000, [0, 1, 2, 3, 4, 5, 6, 7]), + ] { + c.set_dma_64bit(15, 0, wide); + assert_eq!(d.dma_write(address, &[0, 1, 2, 3, 4, 5, 6, 7]), PciDmaStatus::Ok); + assert_eq!(&mem.0.lock()[target..target + 8], &expected); + } + let records = Arc::new(Mutex::new(Vec::new())); + let out = records.clone(); + c.set_dma_sender(Arc::new(move |p, b| { + out.lock().push((p, b.to_vec())); + true + })); + c.set_wide_slot_mask(15, 1 << 3); + c.set_dma_64bit(15, 3, true); + f.write32((reg::CONTROL + 4) as u64, 8); + f.write32((reg::DIRECT_MAP + 4) as u64, 10 << 20); + f.write32((reg::DEVICE_BASE + 3 * 8 + 4) as u64, 1 << 19); + assert_eq!( + c.dma(15, 3) + .dma_write(0x80002000, &[0, 1, 2, 3, 4, 5, 6, 7]), + PciDmaStatus::Ok + ); + let r = records.lock(); + assert_eq!(r.len(), 1); + assert_eq!(r[0].0.source_widget, 8); + assert_eq!(r[0].0.slot, 3); + assert_eq!(r[0].0.widget, 10); + assert_eq!(r[0].0.address, 0x2000); + assert!(r[0].0.half_write_requested); + assert_eq!(r[0].1, [0, 1, 2, 3, 4, 5, 6, 7]); + drop(r); + c.set_dma_sender(Arc::new(|p, b| { + if p.address >= 0x2008 { + return false; + } + b.fill(0x42); + true + })); + let mut bytes = [0xff; 16]; + assert_eq!( + c.dma(15, 0).dma_read(0x80002000, &mut bytes), + PciDmaStatus::MasterAbort + ); + assert_eq!( + bytes, + [0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0, 0, 0, 0, 0, 0, 0, 0] + ); +} +/// A PCI function may inspect XBridge from its link-reset callback. +#[test] +fn link_reset_callback_resets_a_function_and_can_reenter_the_chip() { + use crate::dev::isp1040::{mbox, reg as isp_reg, CTRL_INT_ENABLE, CTRL_RISC_INT_ENABLE, HOST_CMD_SHIFT}; + use crate::dev::isp12160::Isp12160; + + let (c, _) = chip(); + let isp = Arc::new(Isp12160::new(Isp12160::new_bus(), Isp12160::new_bus())); + c.attach(15, 1, isp.clone()); + let (probe, line) = crate::irq::LevelProbe::line(); + c.set_pci_irq(15, 1, 0, line); + isp.config().write(1, 6); + isp.config().write(5, 0x02000000); + isp.bar_write(1, isp_reg::BUS_CTRL, u32::from(CTRL_INT_ENABLE | CTRL_RISC_INT_ENABLE), 2); + isp.bar_write(1, isp_reg::MAILBOX_BASE, u32::from(mbox::NOP), 2); + isp.bar_write(1, isp_reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT, 2); + assert!(probe.level()); + assert_eq!(isp.bar_read(1, isp_reg::MAILBOX_BASE, 2), u32::from(mbox::COMPLETE)); + + let weak = Arc::downgrade(&c); + let reset_isp = isp.clone(); + c.set_reset_output(Arc::new(move |port| { + assert_eq!(port, 15); + let chip = weak.upgrade().unwrap(); + // Callbacks own attached engines and may inspect or rewire the chip. + assert_eq!(chip.head(port).read32((reg::DIRECT_MAP + 4) as u64).data, 0x20000); + assert_eq!(chip.crossbar().read32((xbow::IDENTIFICATION + 4) as u64).data, 0x2d000001); + assert_eq!(reset_isp.config().command(), 0); + reset_isp.power_on(); + chip.set_reset_output(Arc::new(|_| {})); + })); + c.head(15).write32((reg::DIRECT_MAP + 4) as u64, 0xa00000); + c.crossbar().write32((xbow::LINK_BASE + 7 * xbow::LINK_STRIDE + xbow::LINK_RESET + 4) as u64, 1); + assert!(!probe.level()); + assert_eq!(isp.config().bar_base(1), 0); + assert_eq!(isp.bar_read(1, isp_reg::BUS_CTRL, 2), 0); + assert_eq!(isp.bar_read(1, isp_reg::BUS_STATUS, 2), 0); + assert_eq!(isp.bar_read(1, isp_reg::MAILBOX_BASE, 2), 0); + assert_eq!(isp.bar_read(1, isp_reg::MAILBOX_BASE + 2, 2), 0x4953); +} + +/// Only admitted guest ports receive warm-reset notifications after both heads reset. +#[test] +fn warm_reset_notifies_only_admitted_ports_after_head_state_is_reset() { + let (c, _) = chip(); + let ports = Arc::new(Mutex::new(Vec::new())); + let output = ports.clone(); + let weak = Arc::downgrade(&c); + c.set_reset_output(Arc::new(move |port| { + if port >= 14 { + assert_eq!(weak.upgrade().unwrap().head(port).read32((reg::DIRECT_MAP + 4) as u64).data, 0x20000); + } + output.lock().push(port); + })); + let x = c.crossbar(); + x.write32((xbow::FENCE_BASE + 8 + 6 * 8 + 4) as u64, 0xff & !(1 << 2)); + c.head(14).write32((reg::DIRECT_MAP + 4) as u64, 0xa00000); + c.head(15).write32((reg::DIRECT_MAP + 4) as u64, 0xa00000); + c.warm_reset(10, 0); + assert_eq!(*ports.lock(), vec![8, 9, 10, 11, 12, 13, 15]); + assert_eq!(c.head(14).read32((reg::DIRECT_MAP + 4) as u64).data, 0xa00000); + ports.lock().clear(); + c.warm_reset(10, 10239); + assert!(ports.lock().is_empty()); + c.warm_reset(10, 10240); + assert_eq!(*ports.lock(), vec![8, 9, 10, 11, 12, 13, 14, 15]); +} +/// The crossbar reports the first error once and retains its programmed destination. +#[test] +fn crossbar_errors_deliver_once_then_clear_the_original_destination() { + let (c, _) = chip(); + let x = c.crossbar(); + let packets = Arc::new(Mutex::new(Vec::new())); + let out = packets.clone(); + c.set_interrupt_sender(Arc::new(move |p| { + out.lock().push(p); + true + })); + x.write32((xbow::INT_UPPER + 4) as u64, 0x550a0000); + x.write32((xbow::INT_LOWER + 4) as u64, 0x80); + x.write32((xbow::CONTROL + 4) as u64, 0x20); + x.read64(xbow::IDENTIFICATION as u64); + x.write32((xbow::IDENTIFICATION + 4) as u64, 1); + assert_eq!(packets.lock().len(), 1); + assert_eq!(x.read32((xbow::STATUS_CLEAR + 4) as u64).data & ((1 << 23) | 32), (1 << 23) | 32); + let p = packets.lock(); + assert_eq!(p.len(), 2); + assert_eq!(p[0].vector, 0x55); + assert_eq!(p[0].widget, 10); + assert_eq!(p[0].address, 0x80); + assert!(p[0].level); + assert!(!p[1].level); +} + +struct Card { + config: crate::pci::PciConfig, + bytes: Mutex>, + irq: Mutex>, +} +impl Card { + fn new() -> Arc { + Self::with_status_events(0) + } + fn with_status_events(mask: u16) -> Arc { + Self::with_bar(crate::pci::Bar::Mem(32), mask) + } + fn with_bar(bar: crate::pci::Bar, mask: u16) -> Arc { + use crate::pci::{Bar, PciConfig}; + let size = match bar { Bar::Mem(size) | Bar::Io(size) => size as usize, Bar::Unused => 0 }; + Arc::new(Self { + config: PciConfig::new_with_policy( + 0x1234, + 0x5678, + 0, + 0, + [ + bar, + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + ], + 1, + crate::pci::PciConfigPolicy { status_w1c_mask: mask, ..Default::default() }, + ), + bytes: Mutex::new((0..size).map(|i| i as u8).collect()), + irq: Mutex::new(None), + }) + } +} +impl PciBusDevice for Card { + fn config(&self) -> &crate::pci::PciConfig { + &self.config + } + fn bar_read(&self, _: usize, off: u32, width: usize) -> u32 { + let b = self.bytes.lock(); + (0..width).fold(0, |v, i| v | ((b[off as usize + i] as u32) << (i * 8))) + } + fn bar_write(&self, _: usize, off: u32, value: u32, width: usize) { + let mut b = self.bytes.lock(); + for i in 0..width { + b[off as usize + i] = (value >> (i * 8)) as u8; + } + } + fn connect_interrupt(&self, line: IrqLine) { + *self.irq.lock() = Some(line); + } +} +/// The guest can identify the first invalid head access and its error destination. +#[test] +fn invalid_register_captures_first_address_and_reports_error_destination() { + let (c, _) = chip(); + let f = c.head(15); + let packets = Arc::new(Mutex::new(Vec::new())); + let out = packets.clone(); + c.set_interrupt_sender(Arc::new(move |p| { + out.lock().push(p); + true + })); + f.write32((reg::INT_DEST_UPPER + 4) as u64, 0xa0000); + f.write32((reg::INT_DEST_LOWER + 4) as u64, 0x80); + f.write32((reg::INTERRUPT_ERROR_HOST + 4) as u64, 73); + f.write32((reg::INTERRUPT_ENABLE + 4) as u64, 1 << 24); + f.read32(0x1000004); + f.read32(0x2000004); + assert_eq!(f.read32((reg::ERR_LOWER + 4) as u64).data, 0x1000004); + assert_eq!(packets.lock().len(), 1); + assert_eq!(packets.lock()[0].vector, 73); + f.write32((reg::INTERRUPT_RESET + 4) as u64, 8); + assert_eq!(f.read32((reg::INTERRUPT_STATUS + 4) as u64).data & (1 << 24), 0); +} +/// Narrow guest configuration writes preserve status bits outside the written lanes. +#[test] +fn partial_configuration_writes_preserve_unwritten_status_events() { + let (c, _) = chip(); + let f = c.head(15); + let card = Card::with_status_events(0xc000); + c.attach(15, 0, card.clone()); + for (bus, expected) in [(0, 0x56781234), (1, u32::MAX)] { + f.write32((reg::PCI_TYPE1_CONFIG + 4) as u64, bus << 16); + assert_eq!(f.read32(0x28000).data, expected); + } + card.config.set_status_bits(0xc000); + assert_eq!(f.write16(0x20006, 0x0102), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0xc200_0102); + assert_eq!(f.write8(0x20007, 3), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0xc200_0103); + assert_eq!(f.write8(0x20006, 2), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0xc200_0203); + assert_eq!(f.write8(0x20005, 0xff), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0xc200_0203); + assert_eq!(f.write8(0x20004, 0x80), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0x4200_0203); + assert_eq!(f.write16(0x20004, 0x4000), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0x0200_0203); + card.config.set_status_bits(0xc000); + assert_eq!(f.write32(0x20004, 0x8000_0002), BUS_OK); + assert_eq!(f.read32(0x20004).data, 0x4200_0002); +} + +fn assert_pio_matches_slow(head: &XBridgeHead, card: &Card, address: u64, mask: usize) { + for size in [1, 2, 4, 8] { + for offset in (0..32).step_by(size) { + let at = address + offset as u64; + let before = card.bytes.lock().clone(); + let expected = (0..size).fold(0u64, |v, i| (v << 8) | before[(offset + i) ^ mask] as u64); + assert_eq!(head.access(at, size, None), expected, "read {at:#x} width {size}"); + assert_eq!(head.slow_pio(at, size, None), expected); + head.access(at, size, Some(0x0123456789abcdef)); + let fast = card.bytes.lock().clone(); + *card.bytes.lock() = before.clone(); + head.slow_pio(at, size, Some(0x0123456789abcdef)); + assert_eq!(*card.bytes.lock(), fast, "write {at:#x} width {size}"); + *card.bytes.lock() = before; + } + } +} + +/// Native BAR dispatch preserves bridge data lanes at every supported access width. +#[test] +fn native_bar_pages_preserve_all_pio_lanes() { + for memory in [false, true] { + let (chip, _) = chip(); + let head = chip.pci_head(15); + let bar = if memory { crate::pci::Bar::Mem(0x2000) } else { crate::pci::Bar::Io(0x2000) }; + let card = Card::with_bar(bar, 0); + // Device window 0 reaches a function owned by slot 3. + chip.attach(15, 3, card.clone()); + head.write32(0x23010, 0x02000000 | u32::from(!memory)); + head.write32(0x23004, if memory { 2 } else { 1 }); + for wide in [false, true] { + chip.set_dma_64bit(15, 3, wide); + for stream in [false, true] { + head.write32((reg::DEVICE_BASE + 4) as u64, 0x20 | if memory { 1 << 12 } else { 0 } + | if wide { 1 << 22 } else { 0 } | if stream { 1 << 13 } else { 0 }); + assert!(head.pio_map.lock().runs.contains(&(0x200000, 0x202000))); + assert_pio_matches_slow(head, &card, 0x200000, if stream { 0 } else if wide { 7 } else { 3 }); + for swap in [false, true] { + head.write32((reg::CONTROL + 4) as u64, if swap { 1 << if memory { 22 } else { 23 } } else { 0 }); + let aliases = if memory { + vec![(0x42000000, false), (0x82000000, wide)] + } else { vec![(0x102000000, false)] }; + for (address, wide) in aliases { + assert_pio_matches_slow(head, &card, address, if swap { 0 } else if wide { 7 } else { 3 }); + } + } + } + } + } +} + +/// A moved or reset BAR stops reaching its former endpoint. +#[test] +fn bar_moves_disable_and_reset_revoke_native_pages_without_new_endpoints() { + let (chip, _) = chip(); + let head = chip.pci_head(15); + let card = Card::with_bar(crate::pci::Bar::Mem(0x1000), 0); + let old_ref = Arc::downgrade(&card); + chip.attach(15, 0, card); + head.write32(0x20010, 0x02000000); + head.write32(0x20004, 2); + head.write32((reg::DEVICE_BASE + 4) as u64, 0x1020); + let original: Vec<_> = head.pio_map.lock().entries.iter() + .map(|(key, entry)| (*key, entry.device.clone())).collect(); + let stale = head.pio_map.lock().entries[&(8, 0, 0)].device.clone(); + head.write32((reg::DEVICE_BASE + 4) as u64, 0x1022); + assert_eq!(head.read32(0x200000).data, u32::MAX); + head.write32((reg::DEVICE_BASE + 4) as u64, 0x1020); + assert_eq!(head.read32(0x200000).data, 0x03020100); + for i in 0..128 { + let base = 0x02000000 + i * 0x1000; + head.write32(0x20010, base); + assert_eq!(head.read32(0x40000000 + base as u64).data, 0x03020100); + if i != 0 { + assert_eq!(head.read32(0x40000000 + base as u64 - 0x1000).data, u32::MAX); + } + } + assert_eq!(stale.read32(0x42000000).data, u32::MAX); + let entries = head.pio_map.lock(); + assert_eq!(entries.entries.len(), original.len()); + for (key, device) in &original { assert!(Arc::ptr_eq(device, &entries.entries[key].device)); } + drop(entries); + head.write8(0x20007, 0); + assert!(head.pio_map.lock().runs.is_empty()); + assert_eq!(head.read32(0x4207f000).data, u32::MAX); + head.write8(0x20007, 2); + assert!(!head.pio_map.lock().runs.is_empty()); + chip.crossbar().write32((xbow::LINK_BASE + 7 * xbow::LINK_STRIDE + xbow::LINK_RESET + 4) as u64, 1); + assert!(head.pio_map.lock().runs.is_empty()); + assert_eq!(head.read32(0x4207f000).data, u32::MAX); + head.write32(0x20010, 0x02000000); + head.write32(0x20004, 2); + let stale = head.pio_map.lock().entries[&(8, 0, 0)].device.clone(); + let replacement = Card::with_bar(crate::pci::Bar::Mem(0x1000), 0); + replacement.bytes.lock().fill(0x5a); + chip.attach(15, 0, replacement); + assert_eq!(head.read32(0x42000000).data, u32::MAX); + assert_eq!(stale.read32(0x42000000).data, u32::MAX); + stale.write32(0x42000000, 0xdeadbeef); + assert_eq!(old_ref.upgrade().unwrap().bytes.lock()[..4], [0, 1, 2, 3]); + head.write32(0x20010, 0x02000000); + head.write32(0x20004, 2); + assert_eq!(head.read32(0x42000000).data, 0x5a5a5a5a); +} + +/// A BAR page shared with another mapping must retain the slow decoder. +#[test] +fn partial_or_overlapping_bar_pages_use_the_slow_decoder() { + let (chip, _) = chip(); + let head = chip.pci_head(15); + let small = Card::new(); + let large = Card::with_bar(crate::pci::Bar::Mem(0x2000), 0); + small.bytes.lock().fill(0xa5); + chip.attach(15, 0, small); + chip.attach(15, 1, large.clone()); + for slot in 0..2 { + head.write32(0x20010 + slot * 0x1000, 0x02000000); + head.write32(0x20004 + slot * 0x1000, 2); + } + head.write32((reg::DEVICE_BASE + 4) as u64, 0x1020); + assert!(head.pio_map.lock().runs.is_empty()); + assert_eq!(head.read32(0x42000000).data, 0xa5a5a5a5); + assert_eq!(head.read32(0x42000020).data, 0x23222120); + head.write32(0x20004, 0); + assert!(!head.pio_map.lock().runs.is_empty()); + assert_pio_matches_slow(head, &large, 0x42000000, 3); + head.write32(0x21004, 0); + head.write32(0x20004, 2); + assert!(head.pio_map.lock().runs.is_empty()); + assert_eq!(head.read32(0x42000000).data, 0xa5a5a5a5); +} + +/// BAR holes and cross-boundary cycles must use the original slow decoder, +/// including after the host publishes a replacement set of BAR runs. +#[test] +fn sparse_bar_lookup_rejects_holes_and_cross_boundary_cycles() { + let decode = PioDecode::default(); + let first: Arc = Arc::new(Memory(Mutex::new(vec![0; 0x4000]))); + let second: Arc = Arc::new(Memory(Mutex::new(vec![0; 0x4000]))); + drop(decode.publish(vec![ + BarRun { start: 0x3000, end: 0x4000, dev: second.clone() }, + BarRun { start: 0x1000, end: 0x2000, dev: first.clone() }, + ])); + assert!(Arc::ptr_eq(&decode.target(0x1000, 8).unwrap(), &first)); + assert!(Arc::ptr_eq(&decode.target(0x1ff8, 8).unwrap(), &first)); + assert!(decode.target(0x1ffc, 8).is_none()); + assert!(decode.target(0x2000, 8).is_none()); + assert!(decode.target(0x2ff8, 8).is_none()); + assert!(Arc::ptr_eq(&decode.target(0x3000, 8).unwrap(), &second)); + assert!(decode.target(0x4000, 8).is_none()); + assert!(decode.target(u64::MAX, 8).is_none()); + drop(decode.publish(vec![BarRun { start: 0x2000, end: 0x3000, dev: first.clone() }])); + assert!(decode.target(0x1000, 8).is_none()); + assert!(Arc::ptr_eq(&decode.target(0x2000, 8).unwrap(), &first)); + assert!(decode.target(0x3000, 8).is_none()); +} + +fn xbow_read(x: &Arc, off: u32) -> u32 { + x.read32((off + 4) as u64).data +} + +/// Software probing the crossbar after reset sees these values. Each +/// link's control word resets to 0x80000; that value belongs in the control +/// word, not in the link reset register, which no read can see. +#[test] +fn the_crossbar_resets_to_its_documented_values() { + let (c, _) = chip(); + let x = c.crossbar(); + for (off, value) in [ + (xbow::ERR_UPPER, 0), + (xbow::ERR_LOWER, 0), + (xbow::CONTROL, 0x80), + (xbow::PACKET_TIMEOUT, 0xfffff), + (xbow::INT_UPPER, 0x80000), + (xbow::INT_LOWER, 0), + (xbow::ERR_CMD, 0), + (xbow::LLP_CONTROL, 0x03ff1810), + (xbow::ARB_RELOAD, 2), + (xbow::PERF_A, 0), + (xbow::PERF_B, 0), + (xbow::NIC, 2), + (xbow::FENCE_BASE, 0xff), + (xbow::LOCK, 0), + ] { + assert_eq!(xbow_read(&x, off), value, "{off:#x}"); + } + for link in 0..8 { + let base = xbow::LINK_BASE + link * xbow::LINK_STRIDE; + assert_eq!(xbow_read(&x, xbow::FENCE_BASE + 8 + link * 8), 0xff); + assert_eq!(xbow_read(&x, base + xbow::LINK_CONTROL), 0x80000, "link {}", link + 8); + assert_eq!(xbow_read(&x, base + xbow::LINK_ARB_UPPER), 0); + assert_eq!(xbow_read(&x, base + xbow::LINK_ARB_LOWER), 0); + } +} + +/// Software shares the crossbar between processors with the resource lock: +/// a write takes it only while it is free, so a second holder is refused +/// until the first writes the clear register. +#[test] +fn the_resource_lock_admits_one_holder_until_cleared() { + let (c, _) = chip(); + let x = c.crossbar(); + let other = c.crossbar_for_port(HEAD_E); + x.write32((xbow::LOCK + 4) as u64, 0x5a); + assert_eq!(xbow_read(&x, xbow::LOCK), 0x5a); + other.write32((xbow::LOCK + 4) as u64, 0x3c); + assert_eq!(xbow_read(&x, xbow::LOCK), 0x5a); + assert_eq!(xbow_read(&x, xbow::LOCK_CLEAR), 0x5a); + x.write32((xbow::LOCK_CLEAR + 4) as u64, 0); + assert_eq!(xbow_read(&x, xbow::LOCK), 0); + other.write32((xbow::LOCK + 4) as u64, 0x3c); + assert_eq!(xbow_read(&x, xbow::LOCK), 0x3c); +} + +/// Every reset fence bit is read and write, a link's own bit included, and +/// only the low eight bits exist. +#[test] +fn reset_fences_keep_their_low_eight_bits() { + let (c, _) = chip(); + let x = c.crossbar(); + for off in (xbow::FENCE_BASE..=FENCE_LAST).step_by(8) { + x.write32((off + 4) as u64, 0); + assert_eq!(xbow_read(&x, off), 0); + x.write32((off + 4) as u64, off as u32); + assert_eq!(xbow_read(&x, off), off as u32 & 0xff); + x.write32((off + 4) as u64, u32::MAX); + assert_eq!(xbow_read(&x, off), 0xff); + } +} From 2ec28039623c8e6bf48bd5bbecc2e7786609b507 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 14/20] l1: add the Fuel system controller The Fuel console first passes through the L1 system controller. Reuse the shared 16550 core and keep controller framing, power commands and identity records in its protocol model. --- debug.md | 5 +- src/dev/l1.rs | 449 ++++++++++++++++++++++++++ src/dev/l1/model.rs | 764 ++++++++++++++++++++++++++++++++++++++++++++ src/dev/l1/tests.rs | 381 ++++++++++++++++++++++ src/dev/mod.rs | 1 + src/devlog.rs | 8 +- 6 files changed, 1604 insertions(+), 4 deletions(-) create mode 100644 src/dev/l1.rs create mode 100644 src/dev/l1/model.rs create mode 100644 src/dev/l1/tests.rs diff --git a/debug.md b/debug.md index 5c7775204..025692a06 100644 --- a/debug.md +++ b/debug.md @@ -593,10 +593,11 @@ Per-module logging; all of it needs a developer build to produce output. | `log file ` | Send a module's log to a file | Modules: `net hpc3 seeq hal2 mc rex3 mips ioc scsi pdma vino dcb vc2 cmap -xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock`. Mask categories: `pdma` — +xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock l1`. Mask categories: `pdma` — `hal enet scsi`; `mips` — `insn tlb mem fpu cp0`; `l1i`/`l1d`/`l2c` — `hit miss op`; every module also takes `on`/`all`, `off`/`none` or a hex mask. -`bedrock` covers the Fuel's devices on its Bedrock hub. +`bedrock` covers the Fuel's devices on its Bedrock hub, and `l1` its system +controller. `IRIS_DEBUG_LOG` or `[debug] debug_log` set the same thing at startup. ### Performance and locks diff --git a/src/dev/l1.rs b/src/dev/l1.rs new file mode 100644 index 000000000..c23b6c256 --- /dev/null +++ b/src/dev/l1.rs @@ -0,0 +1,449 @@ +//! Fuel's L1 system controller and its processor-side UART. +//! +//! The internal L1 connection is separate from IOC3's external serial ports. +//! The Ns16550 owns the UART registers, FIFO, timeout and interrupt state; +//! this device keeps the controller protocol and its eight-byte register stride. +//! Controller identities and power transitions are functional approximations. +//! Remote routing and controller diagnostics are absent. +//! +//! Ported from hw/misc/sgi-l1.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::ns16550::{com_reg, Ns16550}; +use crate::dev::serial_host::{null_serial_backend, SerialBackend}; +use crate::devlog::LogModule; +use crate::irq::IrqLine; +use crate::traits::{BusDevice, BusRead64, Device, Resettable, BUS_ERR, BUS_OK}; +use parking_lot::Mutex; +use std::collections::VecDeque; +use std::io; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, +}; + +mod model; +use model::State; +pub use model::{parse_revision, L1Config, L1Event, DEFAULT_BRICK_SERIAL, DEFAULT_REVISION}; + +fn fcs(mut crc: u16, byte: u8) -> u16 { + crc ^= byte as u16; + for _ in 0..8 { + crc = (crc >> 1) ^ if crc & 1 != 0 { 0x8408 } else { 0 }; + } + crc +} + +fn encode(payload: &[u8]) -> Vec { + let crc = !payload.iter().fold(0xffff, |value, byte| fcs(value, *byte)); + let mut out = vec![0x7e]; + for byte in payload.iter().copied().chain([crc as u8, (crc >> 8) as u8]) { + if matches!(byte, 0x7e | 0x7d | 0x0d) { + out.extend_from_slice(&[0x7d, byte ^ 0x20]); + } else { + out.push(byte); + } + } + out.push(0x7e); + out +} + +const UART_BAUDBASE: u32 = 115200; +const UART_WINDOW_SIZE: u64 = 0x40; + +mod uart_offset { + use super::com_reg; + pub const DATA: u64 = 0x00; + pub const IER: u64 = 0x08; + pub const IIR_FCR: u64 = 0x10; + pub const LCR: u64 = 0x18; + pub const MCR: u64 = 0x20; + pub const LSR: u64 = 0x28; + pub const MSR: u64 = 0x30; + pub const SCR: u64 = 0x38; + + pub fn register(offset: u64) -> u32 { + match offset { + DATA => com_reg::THR, + IER => com_reg::IER, + IIR_FCR => com_reg::IIR_FCR, + LCR => com_reg::LCR, + MCR => com_reg::MCR, + LSR => com_reg::LSR, + MSR => com_reg::MSR, + SCR => com_reg::SCR, + _ => unreachable!("validated L1 UART offset"), + } + } +} + +type EventHandler = Arc; + +#[derive(Default)] +struct Dispatch { + active: bool, + pending: bool, + level: Option, +} + +/// The UART backend does not connect guest L1 commands to a host terminal. +/// Guest transmit bytes are handed to the protocol parser after the UART lock +/// is released; receive bytes come from the protocol reply queue. +struct ProtocolBackend { + guest_tx: Arc>>, + protocol_rx: Arc>>, +} + +impl SerialBackend for ProtocolBackend { + fn send_byte(&self, byte: u8) { + self.guest_tx.lock().push_back(byte); + } + + fn recv_byte(&self) -> io::Result { + self.protocol_rx + .lock() + .pop_front() + .ok_or_else(|| io::ErrorKind::WouldBlock.into()) + } +} + +/// The internal L1 connection is independent of IOC3's external serial ports. +/// Register offsets are relative to the 0x40-byte UART window. Only aligned +/// 64-bit accesses are decoded; register values occupy the low eight bits. +pub struct L1 { + // UART RX takes UART -> protocol queue. Model replies take state -> protocol + // queue. No path holds the protocol queue while acquiring either lock. + state: Arc>, + // Take dispatch before state when draining, release it before the UART or callbacks. + dispatch: Arc>, + uart: Arc>, + guest_tx: Arc>>, + backend: Arc>>, + irq: Arc>, + handler: Arc>>, + running: Arc, + clock: Arc, + worker: Mutex>>, + wake: Arc<(Mutex<()>, parking_lot::Condvar)>, +} +impl L1 { + pub fn new(config: L1Config) -> Result { + if config.rack > 0x3ff || config.bay > 0x3f || config.node_position > 3 { + return Err("L1 rack, bay or node position out of range".into()); + } + if config.mac == [0; 6] || config.mac[0] & 1 != 0 { + return Err("L1 MAC must be nonzero unicast".into()); + } + if config + .brick_serial + .as_ref() + .is_some_and(|s| s.len() > 37 || s.as_bytes().contains(&0)) + { + return Err("L1 brick serial must fit a 37-byte text field".into()); + } + + let protocol_rx = Arc::new(Mutex::new(VecDeque::new())); + let state = Arc::new(Mutex::new(State::new(config, protocol_rx.clone()))); + let guest_tx = Arc::new(Mutex::new(VecDeque::new())); + let protocol_backend = Arc::new(ProtocolBackend { + guest_tx: guest_tx.clone(), + protocol_rx, + }); + let mut uart = Ns16550::new(UART_BAUDBASE); + uart.set_backend(protocol_backend); + + Ok(Self { + state, + dispatch: Arc::new(Mutex::new(Dispatch::default())), + uart: Arc::new(Mutex::new(uart)), + guest_tx, + backend: Arc::new(Mutex::new(null_serial_backend())), + irq: Arc::new(Mutex::new(IrqLine::unconnected())), + handler: Arc::new(Mutex::new(None)), + running: Arc::new(AtomicBool::new(false)), + clock: Arc::new(AtomicU64::new(0)), + worker: Mutex::new(None), + wake: Arc::new((Mutex::new(()), parking_lot::Condvar::new())), + }) + } + + /// Install the separate host-side L1 console connection. + pub fn set_serial_backend(&self, backend: Arc) { + *self.backend.lock() = backend; + } + + pub fn set_irq(&self, line: IrqLine) { + *self.irq.lock() = line; + self.dispatch.lock().level = None; + self.flush(); + } + + /// Power/reset requests reach the machine only after the reply is queued + /// and after the device state lock is released. + pub fn set_event_handler(&self, handler: EventHandler) { + *self.handler.lock() = Some(handler); + } + + #[cfg(test)] + pub fn node_powered(&self) -> bool { + self.state.lock().powered + } + + pub fn station_mac(&self) -> [u8; 6] { + self.state.lock().config.mac + } + + #[cfg(test)] + pub fn display_line(&self) -> String { + self.state.lock().display.clone() + } + + #[cfg(test)] + pub fn record(&self, component: usize, area: u32) -> Option> { + let index = match area { + 1 => 0, + 2 => 1, + 4 => 2, + _ => return None, + }; + self.state.lock().records.get(component).and_then(|r| { + if r[index].is_empty() { + None + } else { + Some(r[index].clone()) + } + }) + } + + /// Install a raw area before execution. It replaces only the named area; + /// station identity remains the configured MAC, as in the Fuel model. + #[cfg(test)] + pub fn set_record(&self, component: usize, area: u32, bytes: Vec) -> Result<(), String> { + let index = match area { + 1 => 0, + 2 => 1, + 4 => 2, + _ => return Err("unsupported L1 area".into()), + }; + if component >= 16 || bytes.len() > 65535 { + return Err("L1 record out of range".into()); + } + self.state.lock().records[component][index] = bytes; + Ok(()) + } + + pub fn send_event(&self, code: u32, number: u32, text: &str) { + self.state.lock().event(code, number, text); + self.service_protocol_uart(); + self.flush(); + } + + /// Deterministic input path shared with the polled host serial backend. + pub fn receive_console(&self, bytes: &[u8]) { + for &byte in bytes { + self.state.lock().console_input(byte); + self.service_protocol_uart(); + self.flush(); + } + } + + pub fn poll_serial(&self) { + let backend = self.backend.lock().clone(); + for _ in 0..256 { + if self.state.lock().queue.lock().len() > 4088 { + break; + } + match backend.recv_byte() { + Ok(byte) => self.receive_console(&[byte]), + Err(error) => { + if error.kind() != io::ErrorKind::WouldBlock { + log::debug!("L1 serial input: {error}"); + } + break; + } + } + } + } + + /// Let the native UART pull reply bytes from the model's shared bounded queue. + fn service_protocol_uart(&self) { + let mut uart = self.uart.lock(); + uart.pump(); + uart.check_timeout(std::time::Instant::now()); + } + + /// Parse guest transmit only after the UART lock is released. The UART + /// callback only appends to this independent queue. + fn process_guest_transmit(&self) { + let bytes: Vec<_> = self.guest_tx.lock().drain(..).collect(); + if bytes.is_empty() { + return; + } + { + let mut state = self.state.lock(); + for byte in bytes { + state.parse(byte); + } + } + self.service_protocol_uart(); + } + + fn flush(&self) { + let mut dispatch = self.dispatch.lock(); + dispatch.pending = true; + if dispatch.active { + return; + } + dispatch.active = true; + loop { + dispatch.pending = false; + let (output, actions) = { + let mut state = self.state.lock(); + ( + std::mem::take(&mut state.output), + std::mem::take(&mut state.actions), + ) + }; + drop(dispatch); + let level = self.uart.lock().irq(); + + // Reentrant reads request another drain, but must not repeat an + // unchanged IRQ and recursively trigger the same machine callback. + dispatch = self.dispatch.lock(); + let changed = dispatch.level != Some(level); + dispatch.level = Some(level); + drop(dispatch); + + if changed { + let irq = self.irq.lock().clone(); + irq.set(level); + } + let backend = self.backend.lock().clone(); + for byte in output { + backend.send_byte(byte); + } + let handler = self.handler.lock().clone(); + for action in actions { + if let Some(handler) = &handler { + handler(action); + } else { + crate::dlog!(LogModule::L1, "unconnected L1 action {action:?}"); + } + } + + dispatch = self.dispatch.lock(); + if !dispatch.pending { + dispatch.active = false; + break; + } + } + } +} +impl BusDevice for L1 { + fn read64(&self, addr: u64) -> BusRead64 { + if addr >= UART_WINDOW_SIZE || addr & 7 != 0 { + return BusRead64::err(); + } + let value = self.uart.lock().read(uart_offset::register(addr)); + self.flush(); + BusRead64::ok(value as u64) + } + + fn write64(&self, addr: u64, value: u64) -> u32 { + if addr >= UART_WINDOW_SIZE || addr & 7 != 0 { + return BUS_ERR; + } + let register = uart_offset::register(addr); + let value = value as u8; + if register == com_reg::IIR_FCR && value & com_reg::FCR_RFR != 0 { + self.state.lock().queue.lock().clear(); + } + self.uart.lock().write(register, value); + self.process_guest_transmit(); + self.flush(); + BUS_OK + } +} +impl Resettable for L1 { + fn power_on(&self) { + self.state.lock().reset(); + self.guest_tx.lock().clear(); + self.uart.lock().reset(); + self.clock.store(0, Ordering::Relaxed); + self.flush(); + } +} +impl Device for L1 { + fn start(&self) { + let mut worker = self.worker.lock(); + if worker.is_some() { + return; + } + self.running.store(true, Ordering::Release); + let device = Self { + state: self.state.clone(), + dispatch: self.dispatch.clone(), + uart: self.uart.clone(), + guest_tx: self.guest_tx.clone(), + backend: self.backend.clone(), + irq: self.irq.clone(), + handler: self.handler.clone(), + running: self.running.clone(), + clock: self.clock.clone(), + worker: Mutex::new(None), + wake: self.wake.clone(), + }; + *worker = Some( + std::thread::Builder::new() + .name("l1-serial".into()) + .spawn(move || { + while device.is_running() { + device.poll_serial(); + let mut guard = device.wake.0.lock(); + if device.is_running() { + device + .wake + .1 + .wait_for(&mut guard, std::time::Duration::from_millis(2)); + } + } + }) + .expect("start L1 serial worker"), + ); + } + + fn stop(&self) { + let mut slot = self.worker.lock(); + { + let _guard = self.wake.0.lock(); + self.running.store(false, Ordering::Release); + self.wake.1.notify_all(); + } + let worker = slot.take(); + if let Some(worker) = worker { + if worker.thread().id() != std::thread::current().id() { + let _ = worker.join(); + } + } + } + + fn is_running(&self) -> bool { + self.running.load(Ordering::Acquire) + } + + fn get_clock(&self) -> u64 { + self.clock.load(Ordering::Relaxed) + } + + fn step(&self, cycles: u64) { + if self.is_running() { + self.clock.fetch_add(cycles, Ordering::Relaxed); + } + } +} +impl Drop for L1 { + fn drop(&mut self) { + self.stop(); + } +} +#[cfg(test)] +mod tests; diff --git a/src/dev/l1/model.rs b/src/dev/l1/model.rs new file mode 100644 index 000000000..907fd59dd --- /dev/null +++ b/src/dev/l1/model.rs @@ -0,0 +1,764 @@ +//! Fuel's L1 controller protocol parser and reply model. +//! +//! This state owns packet framing, controller identity records, console +//! commands and power events. The UART register file and receive pacing live +//! in the shared Ns16550 core. +//! +//! Ported from hw/misc/sgi-l1.c in jamesbraid/qemu-sgi-origami. + +use super::*; + +/// Firmware revision the model reports for request 0x0005. +/// +/// Model choice: 1.48.1 is the revision a retained real Fuel L1 session +/// prints (the Fuel contract's L1 section). The model reproduces that +/// firmware's replies, not its code, so the revision is a label. +pub const DEFAULT_REVISION: [u32; 3] = [1, 48, 1]; + +/// Brick serial the model reports for request 0x0004. +/// +/// Model choice: the marked synthetic text form, system-board letter `N`, +/// instance 0 (the component taxonomy's synthetic serials). The real format +/// is unknown, and the value must not pass as a real SGI serial. +pub const DEFAULT_BRICK_SERIAL: &str = "EMUN00"; + +/// Parse a "major.minor.patch" revision string, as a `[fuel] l1_revision` +/// override spells it. Each part is a decimal number. +pub fn parse_revision(text: &str) -> Result<[u32; 3], String> { + let bad = || format!("L1 revision \"{text}\" must be major.minor.patch, each a decimal number"); + let mut parts = text.split('.'); + let mut rev = [0u32; 3]; + for slot in &mut rev { + let part = parts.next().ok_or_else(bad)?; + if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) { + return Err(bad()); + } + *slot = part.parse().map_err(|_| bad())?; + } + if parts.next().is_some() { + return Err(bad()); + } + Ok(rev) +} + +#[derive(Clone, Debug)] +pub struct L1Config { + pub rack: u32, + pub bay: u32, + pub node_position: u8, + pub type_code: u32, + pub revision: Option<[u32; 3]>, + pub mac: [u8; 6], + pub brick_serial: Option, + pub auto_power: bool, +} +impl Default for L1Config { + fn default() -> Self { + Self { + rack: 1, + bay: 1, + node_position: 0, + type_code: 0x41, + revision: Some(DEFAULT_REVISION), + mac: [8, 0, 0x69, 0x12, 0x34, 0x56], + brick_serial: Some(DEFAULT_BRICK_SERIAL.into()), + auto_power: true, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum L1Event { + Power(bool), + Reset, + SoftReset, + Nmi, + Shutdown, +} + +pub(super) struct State { + pub config: L1Config, + pub queue: Arc>>, + frame: Vec, + in_frame: bool, + escape: bool, + overflow: bool, + pub display: String, + pub records: [[Vec; 3]; 16], + pub actions: Vec, + pub output: Vec, + pub powered: bool, + console: bool, + escaped: bool, + command: Vec, + port: u8, + debug: u32, + partition: u32, + two_bit: u32, + reasons: [u32; 2], + events: Option, + power_down_pending: bool, +} +fn text_field(out: &mut Vec, text: &[u8]) { + out.push(1); + out.extend_from_slice(text); + out.push(0); +} +pub(super) fn ints(values: &[u32]) -> Vec { + let mut out = Vec::new(); + for v in values { + out.push(0); + out.extend_from_slice(&v.to_be_bytes()); + } + out +} +fn word(p: &[u8]) -> u32 { + u32::from_be_bytes(p[..4].try_into().unwrap()) +} +fn arguments(mut p: &[u8], count: u8) -> Option> { + let mut fields = Vec::new(); + for _ in 0..count { + let tag = *p.first()?; + let len = match tag { + 0 => 5, + 1 => p[1..].iter().position(|b| *b == 0)? + 2, + t if t & 0x80 != 0 => 1 + (t & 0x7f) as usize, + _ => return None, + }; + fields.push(p.get(..len)?); + p = &p[len..]; + } + if p.is_empty() { + Some(fields) + } else { + None + } +} +fn area_finish(mut area: Vec) -> Vec { + while (area.len() + 1) % 8 != 0 { + area.push(0); + } + let sum = area.iter().fold(0u8, |s, b| s.wrapping_add(*b)); + area.push(sum.wrapping_neg()); + area +} +fn ia_text(area: &mut Vec, text: &str) { + area.push(0xc0 | text.len() as u8); + area.extend_from_slice(text.as_bytes()); +} +fn board(mac: Option<[u8; 6]>) -> Vec { + let mut b = vec![0; 6]; + for s in ["SYNTHETIC", "IP34", "EMUN00", "030-1707-003"] { + ia_text(&mut b, s); + } + b.push(0); + ia_text(&mut b, "-G"); + b.extend_from_slice(&[1, 0]); + ia_text(&mut b, "00"); + for _ in 0..3 { + b.extend_from_slice(&[4, 0, 0, 0, 0]); + } + if let Some(mac) = mac { + ia_text( + &mut b, + &mac.iter().map(|v| format!("{v:02X}")).collect::(), + ); + } + b.push(0xc1); + // The count includes the checksum and padding, not a separate header. + b[1] = (b.len() + 1).div_ceil(8) as u8; + area_finish(b) +} +impl State { + pub fn new(config: L1Config, queue: Arc>>) -> Self { + let mut records: [[Vec; 3]; 16] = + std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); + let chassis = area_finish(vec![0, 2, 0, 0xc2, b'N', b'A', 0xc2, b'N', b'A', 0xc1]); + for c in 0..2 { + records[c][0] = chassis.clone(); + records[c][1] = board(if c == 1 { Some(config.mac) } else { None }); + } + let powered = config.auto_power; + let port = config.node_position << 5; + Self { + config, + queue, + frame: Vec::new(), + in_frame: false, + escape: false, + overflow: false, + display: "L1 running".into(), + records, + actions: Vec::new(), + output: Vec::new(), + powered, + console: powered, + escaped: false, + command: Vec::new(), + port, + debug: 0, + partition: 0, + two_bit: 0, + reasons: [0; 2], + events: None, + power_down_pending: false, + } + } + pub fn reset(&mut self) { + // Controller settings survive the controller's local boot. The UART and + // packet parser reset independently of those persistent settings. + let persistent = (self.debug, self.partition, self.two_bit, self.reasons); + let events = self.events.filter(|&channel| channel >= 4); + let records = std::mem::replace( + &mut self.records, + std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())), + ); + let queue = self.queue.clone(); + queue.lock().clear(); + *self = Self::new(self.config.clone(), queue); + (self.debug, self.partition, self.two_bit, self.reasons) = persistent; + self.events = events; + self.records = records; + } + fn emit(&mut self, payload: &[u8]) { + if payload.len() > 128 { + crate::dlog!(LogModule::L1, "L1 reply exceeds frame capacity"); + return; + } + let bytes = encode(payload); + let mut queue = self.queue.lock(); + if queue.len() + bytes.len() <= 4096 { + queue.extend(bytes); + } else { + crate::dlog!(LogModule::L1, "L1 receive queue full"); + } + } + fn reply(&mut self, channel: u8, code: i32, count: u8, fields: &[u8]) { + let mut p = vec![0x20 | channel]; + p.extend_from_slice(&code.to_be_bytes()); + p.push(count); + p.extend_from_slice(fields); + self.emit(&p); + } + pub fn parse(&mut self, mut byte: u8) { + if byte == 0x7e { + if self.in_frame + && !self.escape + && !self.overflow + && self.frame.len() >= 3 + && self.frame.iter().fold(0xffff, |v, b| fcs(v, *b)) == 0xf0b8 + { + let payload = self.frame[..self.frame.len() - 2].to_vec(); + self.request(&payload); + } + self.in_frame = true; + self.escape = false; + self.overflow = false; + self.frame.clear(); + return; + } + if !self.in_frame || self.overflow { + return; + } + if byte == 0x7d { + self.escape = true; + return; + } + if self.escape { + byte ^= 0x20; + self.escape = false; + } + if self.frame.len() == 258 { + self.overflow = true; + } else { + self.frame.push(byte); + } + } + fn eeprom(&mut self, channel: u8, a: &[&[u8]]) { + if a.len() < 4 || a[..4].iter().any(|v| v[0] != 0) { + self.reply(channel, -100, 0, &[]); + return; + } + let component = word(&a[0][1..]) as usize; + let area = word(&a[1][1..]); + let offset = word(&a[2][1..]) as usize; + let requested = word(&a[3][1..]) as usize; + if component >= 16 || self.records[component].iter().all(Vec::is_empty) { + self.reply(channel, -104, 0, &[]); + return; + } + if area > 4 { + self.reply(channel, -105, 0, &[]); + return; + } + let index = match area { + 1 => 0, + 2 => 1, + 4 => 2, + _ => { + self.reply(channel, -104, 0, &[]); + return; + } + }; + let record = &self.records[component][index]; + if record.is_empty() { + self.reply(channel, -104, 0, &[]); + return; + } + let checksum_len = if area == 4 { + record.len().min(64) + } else { + record.len() + }; + if record[..checksum_len] + .iter() + .fold(0u8, |s, b| s.wrapping_add(*b)) + != 0 + { + self.reply(channel, -107, 0, &[]); + return; + } + if offset >= record.len() { + self.reply(channel, -105, 0, &[]); + return; + } + let len = requested.min(record.len() - offset); + if len > 116 { + crate::dlog!(LogModule::L1, "L1 EEPROM reply exceeds frame capacity"); + self.reply(channel, -105, 0, &[]); + return; + } + let mut fields = ints(&[(record.len() - offset - len) as u32]); + if requested != 0 { + fields.push(0x80 | len as u8); + fields.extend_from_slice(&record[offset..offset + len]); + } + self.reply(channel, 0, if requested == 0 { 1 } else { 2 }, &fields); + } + fn request(&mut self, p: &[u8]) { + let channel = p[0] & 31; + if p[0] & 0xe0 == 0x40 { + self.port = (self.port & 0x60) | channel; + if self.console && self.powered { + self.output.extend_from_slice(&p[1..]); + } else { + crate::dlog!(LogModule::L1, "L1 console output while operator is in L1 mode"); + } + return; + } + if p[0] & 0xe0 != 0 || p.len() < 7 { + return; + } + let address = word(&p[1..]); + let cmd = u16::from_be_bytes([p[5], p[6]]); + let key = address >> 12; + if key != 0xffff && key != ((self.config.rack << 6) | self.config.bay) { + crate::dlog!(LogModule::L1, "L1 unsupported destination {address:#x}"); + self.emit(&[0x20 | channel, 255, 255, 255, 255, 0, 0, 0, 8]); + return; + } + let count = if p.len() == 7 { 0 } else { p[7] }; + let raw = if p.len() > 8 { &p[8..] } else { &[] }; + let task = address & 31; + if task == 5 || task == 21 { + if self.powered { + self.emit(&[ + ((address >> 5) & 31) as u8, + raw.get(4).copied().unwrap_or(0), + ]); + } else { + let mut field = Vec::new(); + text_field( + &mut field, + format!( + "WARNING: power on {:03}c{:02} appears off!", + self.config.rack, self.config.bay + ) + .as_bytes(), + ); + self.reply(channel, -1, 1, &field); + } + return; + } + if task != 6 && task != 3 && task != 18 { + crate::dlog!(LogModule::L1, "L1 unsupported task {task}"); + return; + } + let Some(a) = arguments(raw, count) else { + self.reply(channel, -100, 0, &[]); + return; + }; + if task == 3 { + if a.len() != 1 || a[0][0] != 1 { + self.reply(channel, -100, 0, &[]); + return; + } + let text = String::from_utf8_lossy(&a[0][1..a[0].len() - 1]); + let (code, output) = self.run_line(&text, false); + let mut fields = Vec::new(); + let mut n = 0; + for line in output.split("\r\n").filter(|s| !s.is_empty()) { + if fields.len() + line.len() + 2 > 121 { + break; + } + text_field(&mut fields, line.as_bytes()); + n += 1; + } + self.reply(channel, code, n, &fields); + return; + } + if task == 18 { + let length = a.iter().take(3).map(|v| v.len()).sum::().min(121); + self.reply(channel, cmd as i32, count.min(3), &raw[..length]); + return; + } + let mut values = Vec::new(); + let mut fields = Vec::new(); + let mut field_count = 0; + let mut error = 0; + match cmd { + 1 => values.push(self.debug), + 2 => values.push((self.config.rack << 6) | self.config.bay), + 3 => { + if self.config.type_code == 0 { + error = -104; + } else { + values.extend([ + (self.config.rack << 6) | self.config.bay, + self.config.type_code, + ]); + } + } + 4 => { + if let Some(serial) = &self.config.brick_serial { + text_field(&mut fields, serial.as_bytes()); + field_count = 1; + } else { + error = -104; + } + } + 5 => { + if let Some(rev) = self.config.revision { + values.extend(rev); + } else { + crate::dlog!(LogModule::L1, "L1 revision metadata not configured"); + error = -104; + } + } + 6 => { + self.eeprom(channel, &a); + return; + } + 7 => error = if a.is_empty() { -100 } else { -104 }, + // The system serial is the station address as twelve uppercase + // hexadecimal digits in address order, no separators or OUI + // added: the text the stock PROM converts straight back into the + // Ethernet address (the Fuel contract's MAC section). The + // uppercase case is the contract's choice for the model. + 8 => { + let mac = self + .config + .mac + .iter() + .map(|b| format!("{b:02X}")) + .collect::(); + text_field(&mut fields, mac.as_bytes()); + field_count = 1; + } + 9 => values.push(self.partition), + 0xa => values.extend([0, 0]), + 0xb => values.push(self.two_bit), + 0xc => values.extend([0; 8]), + 0xd => values.extend([0; 4]), + 0xe => values.push((self.port >> 5) as u32 & 3), + 0xf => values.extend(self.reasons), + 0x10 => values.extend([1, 1, 1, 1]), + 0x11 => { + error = -106; + values.push(1); + } + 0x1001 | 0x1002 | 0x1006 | 0x1007 | 0x1008 => { + if a.is_empty() { + error = -100; + } else if a[0][0] != 0 { + error = -105; + } else { + let v = word(&a[0][1..]); + match cmd { + 0x1001 => self.debug = v & 0xffff, + 0x1002 => self.port = ((self.port as u32 & 0x60) | v) as u8 & 0x7f, + 0x1006 => self.partition = v, + 0x1007 => self.events = Some(v as u8), + _ => self.two_bit = v & 3, + } + } + } + 0x1003 => {} + 0x1004 | 0x1005 => { + if a.is_empty() { + error = -100; + } else if a[0][0] != 1 { + error = -102; + } else { + self.display = a[0][1..a[0].len() - 1] + .iter() + .take(72) + .map(|b| { + if (0x20..=0x7e).contains(b) { + *b as char + } else { + '?' + } + }) + .collect(); + } + } + 0x1009 => { + if a.len() < 2 { + error = -100; + } else if a[0][0] != 0 || a[1][0] != 0 || word(&a[1][1..]) & 15 > 2 { + error = -105; + } + } + 0x100a => self.events = None, + 0x100b => { + if a.len() != 3 { + error = -100; + } else { + self.events = None; + } + } + 0x100c => { + if a.len() != 2 { + error = -100; + } + } + 0x2001 | 0x2005 | 0x2006 => { + self.reply(channel, 0, 0, &[]); + match cmd { + 0x2001 => self.reset_node(), + 0x2005 => self.power_down(true, false), + _ => { + self.power_down(false, false); + self.power_up(true); + } + } + return; + } + 0x2002..=0x2004 => error = -100, + 0x2007 | 0x2008 => error = -108, + 0x4000 => error = -104, + 0x4002 => { + self.reply(channel, 0, 0, &[]); + self.reboot(); + return; + } + _ => { + crate::dlog!(LogModule::L1, "L1 unsupported request {cmd:#x}"); + error = -101; + } + } + if !values.is_empty() { + field_count = values.len() as u8; + fields = ints(&values); + } + self.reply(channel, error, field_count, &fields); + } + fn reason(&mut self) { + self.reasons[1] = self.reasons[0]; + self.reasons[0] = 3; + } + fn reset_node(&mut self) { + if self.powered { + self.reason(); + self.display = "Reset done".into(); + self.actions.push(L1Event::Reset); + } + } + fn power_down(&mut self, shutdown: bool, confirm: bool) { + if self.powered { + if confirm && !self.power_down_pending { + self.power_down_pending = true; + self.event( + 0x8000, + 0, + "Power Down: issue 'pwr d' again to immediately power down.", + ); + return; + } + self.power_down_pending = false; + self.reason(); + self.powered = false; + self.display = "Powered Down".into(); + self.actions.push(L1Event::Power(false)); + if shutdown { + self.actions.push(L1Event::Shutdown); + } + } + } + fn power_up(&mut self, restart: bool) { + self.reason(); + self.powered = true; + self.power_down_pending = false; + self.port &= 0x60; + self.console = true; + self.display = "Powered Up".into(); + self.actions.push(L1Event::Power(true)); + if restart { + self.actions.push(L1Event::Reset); + } + } + fn reboot(&mut self) { + self.power_down_pending = false; + self.console = false; + self.escaped = false; + self.command.clear(); + self.frame.clear(); + self.in_frame = false; + self.port = self.config.node_position << 5; + self.display = "L1 running".into(); + if self.events.is_some_and(|c| c < 4) { + self.events = None; + } + } + pub fn event(&mut self, code: u32, number: u32, text: &str) { + let Some(channel) = self.events else { + return; + }; + let mut p = vec![0x40 | (channel & 31)]; + p.extend_from_slice( + &((self.config.rack << 18) | (self.config.bay << 12) | 4).to_be_bytes(), + ); + p.extend_from_slice(&code.to_be_bytes()); + p.push(2); + p.extend(ints(&[number])); + text_field( + &mut p, + format!( + "\r\n{:03}c{:02} ATTN: {text}\r\n", + self.config.rack, self.config.bay + ) + .as_bytes(), + ); + if p.len() <= 127 { + self.emit(&p); + } + } + fn run_line(&mut self, line: &str, interactive: bool) -> (i32, String) { + let words = line.split_whitespace().collect::>(); + let local = if words.first() == Some(&"*") { + &words[1..] + } else { + &words[..] + }; + match local { + ["pwr" | "power", "d" | "down"] => { + self.power_down(true, interactive); + (0, String::new()) + } + ["pwr" | "power", "u" | "up"] => { + if !self.powered { + self.power_up(true); + } + (0, String::new()) + } + ["reset"] => { + self.reset_node(); + (0, String::new()) + } + ["softreset"] => { + self.actions.push(L1Event::SoftReset); + self.display = "Sft rst done".into(); + (0, String::new()) + } + ["nmi"] => { + self.actions.push(L1Event::Nmi); + self.display = "NMI done".into(); + (0, String::new()) + } + ["reboot_l1"] => { + self.reboot(); + (0, String::new()) + } + ["l1"] => { + self.escaped = false; + (0, String::new()) + } + ["version"] => { + if let Some(r) = self.config.revision { + ( + 0, + format!("L1 Controller: Revision {}.{}.{}\r\n", r[0], r[1], r[2]), + ) + } else { + ( + -104, + "L1 firmware revision metadata not configured\r\n".into(), + ) + } + } + ["debug"] if words.len() == 1 => ( + 0, + format!("debug switches set to 0x{:04x}\n\r\n", self.debug), + ), + ["debug", v] if words.len() == 2 => { + let n = if let Some(hex) = v.strip_prefix("0x") { + u32::from_str_radix(hex, 16) + } else { + v.parse() + }; + match n { + Ok(v) => { + self.debug = v & 0xffff; + ( + 0, + format!("debug switches set to 0x{:04x}\n\r\n", self.debug), + ) + } + Err(_) => (-1200, "ERROR: command not found.\n\r\n".into()), + } + } + _ => { + crate::dlog!(LogModule::L1, "L1 unsupported operator command {line:?}"); + (-1, "ERROR: command not found.\r\n".into()) + } + } + } + fn prompt(&mut self) { + self.output.extend_from_slice( + format!("{:03}c{:02}-L1>", self.config.rack, self.config.bay).as_bytes(), + ); + } + pub fn console_input(&mut self, byte: u8) { + if self.console { + if byte == 0x14 { + self.console = false; + self.escaped = true; + self.output + .extend_from_slice(b"escaping to L1 system controller\r\n"); + self.prompt(); + } else if self.powered { + self.emit(&[self.port & 31, byte]); + } else { + crate::dlog!(LogModule::L1, "L1 console input while powered off"); + } + } else if byte == 4 { + self.command.clear(); + self.console = true; + } else if byte == b'\r' || byte == b'\n' { + let line = String::from_utf8_lossy(&self.command).into_owned(); + self.command.clear(); + let (_, output) = self.run_line(&line, true); + self.output.extend_from_slice(output.as_bytes()); + if self.escaped { + self.console = true; + } else { + self.prompt(); + } + } else if byte == 8 || byte == 127 { + self.command.pop(); + } else if self.command.len() < 255 { + self.command.push(byte); + } + } +} diff --git a/src/dev/l1/tests.rs b/src/dev/l1/tests.rs new file mode 100644 index 000000000..64d206fd5 --- /dev/null +++ b/src/dev/l1/tests.rs @@ -0,0 +1,381 @@ +// Cases follow tests/qtest/sn1-l1-test.c in jamesbraid/qemu-sgi-origami: +// uart, frames, identity, eeprom, errors and reset. + +use super::*; + +fn send(l1: &L1, bytes: &[u8]) { + for &b in bytes { + assert_eq!(l1.write64(0, b as u64), BUS_OK); + } +} +fn drain(l1: &L1) -> Vec { + let mut out = Vec::new(); + while l1.read64(0x28).data & 1 != 0 { + out.push(l1.read64(0).data as u8); + } + out +} +fn request(l1: &L1, command: u16, args: &[u8], count: u8) -> Vec { + let mut p = vec![ + 0x10, + 0x0f, + 0xff, + 0xf0, + 6, + (command >> 8) as u8, + command as u8, + count, + ]; + p.extend_from_slice(args); + send(l1, &encode(&p)); + drain(l1) +} +fn integer_reply(values: &[u32]) -> Vec { + let mut p = vec![0x30, 0, 0, 0, 0, values.len() as u8]; + p.extend(model::ints(values)); + encode(&p) +} + +/// The PROM accesses the L1 UART through byte-sized register operations. +#[test] +fn the_l1_uart_frames_bytes_and_prioritizes_interrupts() { + let l1 = L1::new(Default::default()).unwrap(); + assert_eq!(l1.read64(1).status, BUS_ERR); + assert_eq!(l1.read8(0).status, BUS_ERR); + assert_eq!(l1.read32(0).status, BUS_ERR); + assert_eq!(l1.read64(0x28).data, 0x60); + send(&l1, &[0x7e, 0x10, 0x0f, 0xff, 0xf0, 6, 0, 0x0e, 0, 0xf9, 0x72, 0x7e]); + assert_eq!(drain(&l1), [0x7e, 0x30, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0x30, 0x98, 0x7e]); + let (irq, line) = crate::irq::LevelProbe::line(); + l1.set_irq(line); + l1.write64(8, 3); + assert!(irq.level()); + assert_eq!(l1.read64(0x10).data, 2); + assert!(!irq.level()); + l1.write64(0x18, 0x80); + l1.write64(0, 42); + l1.write64(8, 7); + assert_eq!(l1.read64(0).data, 42); + assert_eq!(l1.read64(8).data, 7); + l1.write64(0x18, 3); + send(&l1, &encode(&[0x10, 0x0f, 0xff, 0xf0, 6, 0, 0x0e, 0])); + assert_eq!(l1.read64(0x10).data, 4); + drain(&l1); + assert_eq!(l1.read64(0x10).data, 2); + assert!(!irq.level()); +} + +/// The guest reads controller identity and checks the EEPROM record checksum. +#[test] +fn the_l1_returns_identity_and_validated_eeprom_records() { + let l1 = L1::new(Default::default()).unwrap(); + assert_eq!(request(&l1, 3, &[], 0), integer_reply(&[65, 65])); + let board = l1.record(1, 2).unwrap(); + assert_eq!(board.iter().fold(0u8, |s, x| s.wrapping_add(*x)), 0); + for text in [b"IP34".as_slice(), b"EMUN00", b"080069123456"] { + assert!(board.windows(text.len()).any(|x| x == text)); + } + let mut expected = vec![0x30, 0, 0, 0, 0, 2]; + expected.extend(model::ints(&[0])); + expected.push(0x80 | board.len() as u8); + expected.extend(board); + assert_eq!(request(&l1, 6, &model::ints(&[1, 2, 0, 116]), 4), encode(&expected)); + for (args, count, error) in [([2, 5, 0, 1], 4, 152), ([1, 5, 0, 1], 4, 151), ([1, 2, 0, 1], 3, 156)] { + assert_eq!(request(&l1, 6, &model::ints(&args), count), encode(&[0x30, 255, 255, 255, error, 0])); + } + l1.power_on(); + assert_eq!(request(&l1, 5, &[], 0), integer_reply(&[1, 48, 1])); +} + +/// A successful reply carrying one text field. +fn text_reply(text: &str) -> Vec { + let mut p = vec![0x30, 0, 0, 0, 0, 1]; + p.push(1); + p.extend_from_slice(text.as_bytes()); + p.push(0); + encode(&p) +} + +/// With no configuration the controller reports the labelled model defaults: +/// firmware 1.48.1 and the synthetic brick serial. +#[test] +fn the_l1_reports_the_default_revision_and_brick_serial() { + let l1 = L1::new(Default::default()).unwrap(); + l1.power_on(); + assert_eq!(request(&l1, 5, &[], 0), integer_reply(&[1, 48, 1])); + assert_eq!(request(&l1, 4, &[], 0), text_reply("EMUN00")); +} + +/// Both identity values can be replaced. +#[test] +fn the_l1_revision_and_brick_serial_can_be_overridden() { + let l1 = L1::new(L1Config { + revision: Some(parse_revision("2.13.40").unwrap()), + brick_serial: Some("EMUN07".into()), + ..Default::default() + }) + .unwrap(); + l1.power_on(); + assert_eq!(request(&l1, 5, &[], 0), integer_reply(&[2, 13, 40])); + assert_eq!(request(&l1, 4, &[], 0), text_reply("EMUN07")); + assert_eq!( + L1::new(L1Config { brick_serial: Some("x".repeat(38)), ..Default::default() }).err().as_deref(), + Some("L1 brick serial must fit a 37-byte text field") + ); +} + +/// The revision override is exactly three decimal numbers. +#[test] +fn the_l1_revision_text_is_three_decimal_parts() { + assert_eq!(parse_revision("1.48.1"), Ok([1, 48, 1])); + for bad in ["", "1", "1.48", "1.48.1.0", "1..1", "a.b.c", "1.48.-1", "1.48.+1", " 1.48.1", "1.48.99999999999"] { + assert!(parse_revision(bad).is_err(), "{bad:?}"); + } +} + +/// Request 0x0008 is the configured station address as twelve uppercase +/// hexadecimal digits in address order, the text the PROM turns back into the +/// Ethernet address. +#[test] +fn the_l1_system_serial_is_the_station_address_in_hex() { + let l1 = L1::new(Default::default()).unwrap(); + l1.power_on(); + assert_eq!(request(&l1, 8, &[], 0), text_reply("080069123456")); + let l1 = L1::new(L1Config { mac: [0x00, 0xA0, 0xc9, 0x1f, 0x02, 0xbe], ..Default::default() }).unwrap(); + l1.power_on(); + assert_eq!(request(&l1, 8, &[], 0), text_reply("00A0C91F02BE")); +} + +/// The L1 parser recovers after malformed frames and preserves escaped console text. +#[test] +fn the_l1_resynchronizes_after_bad_frames_and_preserves_escaped_text() { + let l1 = L1::new(Default::default()).unwrap(); + send(&l1, &[0x7e, 1, 2, 3, 0x7e]); + send(&l1, &vec![0x41; 259]); + send(&l1, &[0x7e]); + assert!(drain(&l1).is_empty()); + assert_eq!(request(&l1, 0x2000, &[], 0), encode(&[0x30, 255, 255, 255, 155, 0])); + send(&l1, &encode(&[0x10, 0x4f, 0xff, 0xf0, 6, 0, 3, 0])); + assert_eq!(drain(&l1), encode(&[0x30, 255, 255, 255, 255, 0, 0, 0, 8])); + let mut text = vec![1]; + text.extend_from_slice(b"hi~}\r\0"); + assert_eq!(request(&l1, 0x1004, &text, 1), integer_reply(&[])); + assert_eq!(l1.display_line(), "hi~}?"); + text.truncate(1); + text.extend_from_slice(b"abcdefghijkl\x01\x7f"); + text.extend(std::iter::repeat_n(b'z', 70)); + text.push(0); + assert_eq!(request(&l1, 0x1004, &text, 1), integer_reply(&[])); + assert_eq!(l1.display_line(), format!("abcdefghijkl??{}", "z".repeat(58))); +} + +/// The PROM may reset L1 during boot; controller settings and channel configuration must remain available. +#[test] +fn l1_reset_clears_uart_state_but_preserves_settings_and_channels() { + for channel in [3, 23] { + let l1 = L1::new(Default::default()).unwrap(); + let (irq, line) = crate::irq::LevelProbe::line(); + l1.set_irq(line); + for (set, value) in [(0x1001, 0x1234), (0x1006, 42), (0x1008, 2), (0x1007, channel)] { + request(&l1, set, &model::ints(&[value]), 1); + } + request(&l1, 0x2001, &[], 0); + let reasons = request(&l1, 0xf, &[], 0); + assert_eq!(reasons, integer_reply(&[3, 0])); + l1.write64(8, 1); + l1.send_event(0x8000, 7, "before reset"); + assert!(irq.level()); + let stale = encode(&[0x10, 0x0f, 0xff, 0xf0, 6, 0, 9, 0]); + send(&l1, &stale[..4]); + l1.write64(0x18, 0x80); + l1.write64(0, 42); + l1.write64(8, 7); + l1.power_on(); + assert!(!irq.level()); + assert_eq!(l1.read64(0x18).data, 0); + assert_eq!(l1.read64(8).data, 0); + assert!(drain(&l1).is_empty()); + l1.write64(0x18, 0x80); + // Ns16550 resets its divisor latch to 0x000c for 9600 baud. + assert_eq!(l1.read64(0).data, 0x0c); + assert_eq!(l1.read64(8).data, 0); + l1.write64(0x18, 0); + send(&l1, &stale[4..]); + assert!(drain(&l1).is_empty()); + for (get, value) in [(1, 0x1234), (9, 42), (0xb, 2)] { + assert_eq!(request(&l1, get, &[], 0), integer_reply(&[value])); + } + assert_eq!(request(&l1, 0xf, &[], 0), reasons); + l1.write64(8, 1); + l1.send_event(0x8000, 7, "after reset"); + assert_eq!(irq.level(), channel == 23); + let emitted = drain(&l1); + assert_eq!(emitted.is_empty(), channel == 3); + assert!(!irq.level()); + } +} + +/// A local console power-down request requires a second confirmation command. +#[test] +fn interactive_power_down_waits_for_a_second_command() { + let l1 = Arc::new(L1::new(Default::default()).unwrap()); + let actions = Arc::new(Mutex::new(Vec::new())); + let observed = actions.clone(); + let weak = Arc::downgrade(&l1); + l1.set_event_handler(Arc::new(move |event| { + assert!(!weak.upgrade().unwrap().node_powered()); + observed.lock().push(event); + })); + request(&l1, 0x1007, &model::ints(&[23]), 1); + l1.receive_console(&[0x14]); + l1.receive_console(b"pwr d\r"); + assert!(l1.node_powered()); + assert!(actions.lock().is_empty()); + let mut warning = vec![0x57, 0, 4, 0x10, 4, 0, 0, 0x80, 0, 2]; + warning.extend(model::ints(&[0])); + warning.push(1); + warning.extend_from_slice(b"\r\n001c01 ATTN: Power Down: issue 'pwr d' again to immediately power down.\r\n\0"); + assert_eq!(drain(&l1), encode(&warning)); + l1.power_on(); + l1.receive_console(&[0x14]); + l1.receive_console(b"pwr d\r"); + assert!(l1.node_powered(), "reset clears the first confirmation"); + drain(&l1); + l1.receive_console(&[0x14]); + l1.receive_console(b"power down\r"); + assert!(!l1.node_powered()); + assert_eq!(*actions.lock(), [L1Event::Power(false), L1Event::Shutdown]); +} + +/// Protocol power-down actions must finish before the L1 request reply. +#[test] +fn protocol_and_task_three_power_down_commands_are_immediate() { + let protocol = vec![0x10, 0x0f, 0xff, 0xf0, 6, 0x20, 5, 0]; + let mut task_three = vec![0x10, 0x0f, 0xff, 0xf0, 3, 0, 0, 1, 1]; + task_three.extend_from_slice(b"pwr d\0"); + for (case, packet) in [ + ("protocol command", protocol), + ("task three console", task_three), + ] { + let l1 = L1::new(Default::default()).unwrap(); + let actions = Arc::new(Mutex::new(Vec::new())); + let observed = actions.clone(); + l1.set_event_handler(Arc::new(move |event| observed.lock().push(event))); + send(&l1, &encode(&packet)); + assert_eq!(drain(&l1), integer_reply(&[]), "{case}"); + assert!(!l1.node_powered(), "{case}"); + assert_eq!( + *actions.lock(), + [L1Event::Power(false), L1Event::Shutdown], + "{case}" + ); + } +} + +/// A CPU may read the UART in its interrupt callback while the worker updates the line. +#[test] +fn l1_worker_irq_delivery_stays_ordered_across_reentrant_cpu_reads() { + use std::sync::mpsc; + struct BlockingIrq { + entered: mpsc::SyncSender<()>, + resume: std::sync::Mutex>, + blocked: AtomicBool, + levels: Mutex>, + device: std::sync::Weak, + } + impl crate::irq::InterruptController for BlockingIrq { + fn set_line(&self, _: u32, level: bool) { + if level && !self.blocked.swap(true, Ordering::AcqRel) { + self.entered.send(()).unwrap(); + self.resume.lock().unwrap().recv().unwrap(); + // A machine interrupt handler may inspect the UART immediately. + assert_eq!(self.device.upgrade().unwrap().read64(0x28).data, 0x60); + } + self.levels.lock().push(level); + } + } + let l1 = Arc::new(L1::new(Default::default()).unwrap()); + let backend = Arc::new(crate::dev::serial_host::CiSerialBackend::new()); + let (entered_tx, entered_rx) = mpsc::sync_channel(1); + let (resume_tx, resume_rx) = mpsc::sync_channel(1); + let irq = Arc::new(BlockingIrq { + entered: entered_tx, + resume: std::sync::Mutex::new(resume_rx), + blocked: AtomicBool::new(false), + levels: Mutex::new(Vec::new()), + device: Arc::downgrade(&l1), + }); + l1.set_irq(IrqLine::new(irq.clone(), 0)); + l1.set_serial_backend(backend.clone()); + l1.write64(8, 1); + l1.start(); + backend.push_host(b"x"); + let entered = entered_rx.recv_timeout(std::time::Duration::from_secs(2)); + if entered.is_ok() { + l1.write64(0x10, 2); + } + resume_tx.send(()).unwrap(); + l1.stop(); + entered.unwrap(); + assert_eq!(*irq.levels.lock(), [false, true, false]); +} + +/// Concurrent worker output and platform actions must preserve their order. +#[test] +fn concurrent_l1_outputs_and_actions_are_delivered_in_order() { + use std::sync::mpsc; + struct BlockingBackend { + entered: mpsc::SyncSender<()>, + resume: std::sync::Mutex>, + output: Mutex>, + } + impl SerialBackend for BlockingBackend { + fn send_byte(&self, byte: u8) { + if byte == b'A' { + self.entered.send(()).unwrap(); + self.resume.lock().unwrap().recv().unwrap(); + } + self.output.lock().push(byte); + } + fn recv_byte(&self) -> std::io::Result { + Err(std::io::ErrorKind::WouldBlock.into()) + } + } + let l1 = Arc::new(L1::new(Default::default()).unwrap()); + let (entered_tx, entered_rx) = mpsc::sync_channel(1); + let (resume_tx, resume_rx) = mpsc::sync_channel(1); + let backend = Arc::new(BlockingBackend { + entered: entered_tx, + resume: std::sync::Mutex::new(resume_rx), + output: Mutex::new(Vec::new()), + }); + l1.set_serial_backend(backend.clone()); + let actions = Arc::new(Mutex::new(Vec::new())); + let captured = actions.clone(); + let weak = Arc::downgrade(&l1); + l1.set_event_handler(Arc::new(move |action| { + // Reentrant register reads must not recursively dispatch notifications. + assert_eq!(weak.upgrade().unwrap().read64(0x28).data, 0x60); + captured.lock().push(action); + })); + { + let mut s = l1.state.lock(); + s.output.extend(b"AB"); + s.actions.push(L1Event::Reset); + } + let worker_device = l1.clone(); + let worker = std::thread::spawn(move || worker_device.flush()); + let entered = entered_rx.recv_timeout(std::time::Duration::from_secs(2)); + if entered.is_ok() { + let mut s = l1.state.lock(); + s.output.extend(b"CD"); + s.actions.push(L1Event::Nmi); + drop(s); + l1.flush(); + } + resume_tx.send(()).unwrap(); + worker.join().unwrap(); + entered.unwrap(); + assert_eq!(*backend.output.lock(), b"ABCD"); + assert_eq!(*actions.lock(), [L1Event::Reset, L1Event::Nmi]); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index db1d9f738..714490145 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -71,5 +71,6 @@ pub mod bedrock_pi; pub mod dp83843; pub mod ics1890; pub mod isp12160; +pub mod l1; pub mod timekeeper; pub mod xbridge; diff --git a/src/devlog.rs b/src/devlog.rs index b9ee4f6fa..406858d4f 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -56,6 +56,7 @@ pub enum LogModule { /// model does not support. Odyssey = 28, Bedrock = 29, + L1 = 30, } // Mask bits shared by L1i / L1d / L2c @@ -64,7 +65,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 30; + pub const COUNT: usize = 31; pub fn from_str(s: &str) -> Option { match s { @@ -98,6 +99,7 @@ impl LogModule { "rad1" => Some(Self::Rad1), "odyssey" => Some(Self::Odyssey), "bedrock" => Some(Self::Bedrock), + "l1" => Some(Self::L1), _ => None, } } @@ -134,6 +136,7 @@ impl LogModule { Self::Rad1 => "rad1", Self::Odyssey => "odyssey", Self::Bedrock => "bedrock", + Self::L1 => "l1", } } @@ -144,7 +147,7 @@ impl LogModule { Self::Vino, Self::Dcb, Self::Vc2, Self::Cmap, Self::Xmap, Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, - Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, + Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, Self::L1, ] } @@ -287,6 +290,7 @@ impl DevLog { ModuleLog::new(), // Rad1 ModuleLog::new(), // Odyssey ModuleLog::new(), // Bedrock + ModuleLog::new(), // L1 ], } } From 87d5bd4a9b7bab8712c96b3d81ce1b2c32de7873 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 15/20] ohci: add the Lucent USB host controller and HID boot devices Fuel enumerates a Lucent OHCI controller on PCI. Provide its registers and descriptor scheduling through the USB function interface while leaving devices unattached in the machine configuration. The frame counter follows the machine clock the board hands the controller, and every register read and write first delivers the frame boundaries that clock has passed. Advancing it only from the board's poll, which runs between CPU batches, left HcFmNumber at zero after the guest's own 1 ms wait had expired, and the Fuel PROM reported that SOF did not start within 2 ms. The sync holds one lock from reading the clock to applying the frames, so a timer thread cannot leave a concurrent reader behind it. Entering UsbOperational from Reset or Suspend re-anchors the frame: it loads HcFmRemaining from HcFmInterval, toggle included, and increments HcFmNumber, with the first SOF one frame later (OpenHCI 1.0a 6.2.1, 7.3.2, 7.3.3). The tests cover that entry with frame numbers at and around the overflow flag. Add two low-speed USB functions for the OHCI root ports: a HID boot keyboard and a HID boot mouse, each with one configuration, one boot interface and one interrupt-IN endpoint. They answer the standard requests (descriptors, SET_ADDRESS taking effect after its status stage, SET_CONFIGURATION, status and interface queries) and the HID class requests (GET/SET_REPORT, GET/SET_IDLE, GET/SET_PROTOCOL), and report host keys and pointer moves as boot reports. Reports queue one per change so a key tapped between two polls still shows both edges. Written from the USB 1.1 and HID 1.11 specifications (boot descriptors from appendix B) and the HID Usage Tables, with no code from another emulator. The wheel, remote wakeup and the host layout translation that the PS/2 path has are left out. Ohci::attach loses its test-only gate so a machine can plug the devices in. --- debug.md | 6 +- src/dev/mod.rs | 2 + src/dev/ohci.rs | 589 +++++++++++++++++++++++++++++++++++++ src/dev/ohci/engine.rs | 223 ++++++++++++++ src/dev/ohci/schedule.rs | 98 +++++++ src/dev/ohci/tests.rs | 554 +++++++++++++++++++++++++++++++++++ src/dev/ps2.rs | 70 ++++- src/dev/usb_hid.rs | 609 +++++++++++++++++++++++++++++++++++++++ src/dev/usb_hid/tests.rs | 448 ++++++++++++++++++++++++++++ src/devlog.rs | 8 +- 10 files changed, 2600 insertions(+), 7 deletions(-) create mode 100644 src/dev/ohci.rs create mode 100644 src/dev/ohci/engine.rs create mode 100644 src/dev/ohci/schedule.rs create mode 100644 src/dev/ohci/tests.rs create mode 100644 src/dev/usb_hid.rs create mode 100644 src/dev/usb_hid/tests.rs diff --git a/debug.md b/debug.md index 025692a06..129695cee 100644 --- a/debug.md +++ b/debug.md @@ -593,11 +593,11 @@ Per-module logging; all of it needs a developer build to produce output. | `log file ` | Send a module's log to a file | Modules: `net hpc3 seeq hal2 mc rex3 mips ioc scsi pdma vino dcb vc2 cmap -xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock l1`. Mask categories: `pdma` — +xmap bt445 scc ps2 rtc eeprom l1i l1d l2c bedrock l1 usb`. Mask categories: `pdma` — `hal enet scsi`; `mips` — `insn tlb mem fpu cp0`; `l1i`/`l1d`/`l2c` — `hit miss op`; every module also takes `on`/`all`, `off`/`none` or a hex mask. -`bedrock` covers the Fuel's devices on its Bedrock hub, and `l1` its system -controller. +`bedrock` covers the Fuel's devices on its Bedrock hub, `l1` its system +controller and `usb` its USB host controller. `IRIS_DEBUG_LOG` or `[debug] debug_log` set the same thing at startup. ### Performance and locks diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 714490145..33c4182d3 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -56,6 +56,7 @@ pub mod seeq8003; pub mod testdev; pub mod timer; pub mod ultra64; +pub mod usb_hid; pub mod vino; pub mod virtio; pub mod wd33c93a; @@ -72,5 +73,6 @@ pub mod dp83843; pub mod ics1890; pub mod isp12160; pub mod l1; +pub mod ohci; pub mod timekeeper; pub mod xbridge; diff --git a/src/dev/ohci.rs b/src/dev/ohci.rs new file mode 100644 index 000000000..2625c5b0f --- /dev/null +++ b/src/dev/ohci.rs @@ -0,0 +1,589 @@ +// Derived from MAME's xbox_usb.cpp and xbox_usb.h (the OHCI host controller), +// BSD-3-Clause, copyright Samuele Zannoli. + +//! The Lucent USS-312 USB controller: two root ports, OHCI registers and +//! descriptor processing through the PCI bridge's DMA interface. +//! +//! Functions plug into a root port with `attach`: the Fuel fits a keyboard and +//! a mouse (`usb_hid`), and the tests drive the descriptor and packet +//! scheduling engine through a test-only function. Isochronous +//! traffic, suspend/resume, external hubs, USB storage and legacy keyboard/SMI +//! routing are unsupported. Scheduling uses synthetic work units rather than +//! wire time. Register behavior follows OpenHCI 1.0a and the USS-312 revision 5 +//! data sheet. +//! +//! Ported from src/mame/shared/xbox_usb.cpp and xbox_usb.h in MAME, +//! revision 2ee9b1e30299b122b131cab0bd7912abc2b095ba. + +use crate::dev::heart::{host_clock, Clock}; +use crate::irq::IrqLine; +use crate::pci::{Bar, PciBusDevice, PciConfig, PciConfigPolicy, PciDma, PCI_INTA}; +use crate::traits::{Device, Resettable}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum Pid { + Setup = 0, + Out = 1, + In = 2, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UsbPacket { + pub endpoint: u8, + pub pid: Pid, + pub toggle: bool, + /// IN requests name capacity; OUT and SETUP carry their bytes. + pub capacity: usize, + pub data: Vec, + pub time_ms: u64, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UsbOutcome { + Ack(Vec), + Nak, + Stall, + NoResponse, + CrcError, + BitStuffError, +} +pub trait UsbFunction: Send + Sync { + fn reset(&self); + fn address(&self) -> u8; + fn low_speed(&self) -> bool { + false + } + fn transact(&self, packet: UsbPacket) -> UsbOutcome; +} + +mod engine; +mod schedule; + +const EVENTS: u32 = 0x4000_007f; +const WDH: u32 = 2; +const UE: u32 = 16; +const RHSC: u32 = 64; +const PPS: u32 = 0x100; +const CHANGES: u32 = 0x1f0000; +#[derive(Clone)] +struct State { + r: [u32; 23], + ports: [Option>; 2], + low_speed: [bool; 2], + reset_ms: [u8; 2], + done_delay: u8, + now_ms: u64, + active_fi: u32, + bit_credit: u64, +} +impl State { + fn new() -> Self { + let mut r = [0; 23]; + r[0] = 0x110; + r[13] = 0x2edf; + r[17] = 0x628; + r[18] = 0x1000_0902; + r[19] = 0x60000; + Self { + r, + ports: [None, None], + low_speed: [false; 2], + reset_ms: [0; 2], + done_delay: 7, + now_ms: 0, + active_fi: 0x2edf, + bit_credit: 0, + } + } + fn soft_reset(&mut self) { + let control = (self.r[1] & 0x300) | 0xc0; + let keep = [self.r[18], self.r[19], self.r[20], self.r[21], self.r[22]]; + let ports = self.ports.clone(); + let low_speed = self.low_speed; + let resets = self.reset_ms; + let time = self.now_ms; + *self = Self::new(); + self.r[1] = control; + self.r[18..23].copy_from_slice(&keep); + self.ports = ports; + self.low_speed = low_speed; + self.reset_ms = resets; + self.now_ms = time; + } + fn irq(&self) -> bool { + self.r[1] & 0x100 == 0 && self.r[4] & 0x80000000 != 0 && self.r[3] & self.r[4] & EVENTS != 0 + } + fn powered(&self, p: usize) -> bool { + self.r[18] & 0x200 != 0 || self.r[21 + p] & PPS != 0 + } + fn individual(&self, p: usize) -> bool { + self.r[18] & 0x100 != 0 && self.r[19] & (1 << (17 + p)) != 0 + } + fn power(&mut self, p: usize, on: bool) { + if self.r[18] & 0x200 != 0 || on { + self.r[21 + p] |= PPS; + if self.ports[p].is_some() && self.r[21 + p] & 1 == 0 { + self.r[21 + p] |= 1 | 0x10000 | if self.low_speed[p] { 0x200 } else { 0 }; + self.r[3] |= RHSC; + } + } else { + let previous = self.r[21 + p]; + self.r[21 + p] &= !(PPS | 0x17); + if previous & 1 != 0 { + self.r[21 + p] |= 0x10000; + } + if previous & 2 != 0 { + self.r[21 + p] |= 0x20000; + } + if previous & 3 != 0 { + self.r[3] |= RHSC; + } + self.reset_ms[p] = 0; + } + } +} + +pub struct Ohci { + config: PciConfig, + state: Mutex, + pass: Mutex<()>, + dma: Mutex>>, + irq: Mutex, + running: AtomicBool, + /// Source of the frame count: the machine's own clock, so the guest's + /// idea of a millisecond and the controller's agree. + time: OnceLock, + /// `time` reading at the last frame boundary delivered; `None` while stopped. + clock: Mutex>, +} +impl Ohci { + pub fn new() -> Arc { + let config = PciConfig::new_with_policy( + 0x11c1, + 0x5802, + 0x0c0310, + 0x10, + [ + Bar::Mem(0x1000), + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + Bar::Unused, + ], + PCI_INTA, + PciConfigPolicy { + status: 0x0200, + status_w1c_mask: 0x8000, + command_mask: 0x0006, + latency_mask: 0, + cache_line_mask: 0, + registers: &[], + }, + ); + config.set_subsystem_identity(0xffff_ffff); + Arc::new(Self { + config, + state: Mutex::new(State::new()), + pass: Mutex::new(()), + dma: Mutex::new(None), + irq: Mutex::new(IrqLine::unconnected()), + running: AtomicBool::new(false), + time: OnceLock::new(), + clock: Mutex::new(None), + }) + } + /// Use `clock` for frame timing. Call before `start`. + pub fn set_clock(&self, clock: Clock) { + assert!( + !self.running.load(Ordering::Acquire) && self.time.set(clock).is_ok(), + "connect the OHCI clock once, before start" + ); + } + fn now(&self) -> u64 { + (self.time.get_or_init(host_clock))() + } + /// Deliver every frame boundary the clock has passed. Every register + /// access calls this, so a read sees the frame count the guest's own + /// clock implies, however late the machine's poll or timer ran. + pub fn sync_clock(&self) { + // Held through delivery: a second caller must not find the boundary + // consumed and read the frame count before the first has applied it. + let mut clock = self.clock.lock(); + let Some(at) = clock.as_mut() else { return }; + let now = self.now(); + let elapsed = now.saturating_sub(*at) / 1_000_000; + *at += elapsed * 1_000_000; + self.advance_frames(elapsed); + } + fn drive_irq(&self) { + loop { + let level = self.state.lock().irq(); + let line = self.irq.lock().clone(); + line.set(level); + // A callback may acknowledge the interrupt or another writer may + // change it while delivery is outside the state lock. + if self.state.lock().irq() == level { + break; + } + } + } + pub fn attach(&self, port: usize, function: Arc) -> Result<(), String> { + if port >= 2 { + return Err("OHCI has two ports".into()); + } + if self.state.lock().ports[port].is_some() { + return Err("OHCI port occupied".into()); + } + let low_speed = function.low_speed(); + function.reset(); + { + let _pass = self.pass.lock(); + let mut s = self.state.lock(); + if s.ports[port].is_some() { + return Err("OHCI port occupied".into()); + } + if s.powered(port) { + s.r[21 + port] |= 1 | 0x10000; + } + s.low_speed[port] = low_speed; + if low_speed { + s.r[21 + port] |= 0x200; + } + s.ports[port] = Some(function); + s.r[3] |= RHSC; + } + self.drive_irq(); + Ok(()) + } + #[cfg(test)] + pub fn detach(&self, port: usize) -> Result<(), String> { + if port >= 2 { + return Err("OHCI has two ports".into()); + } + { + let _pass = self.pass.lock(); + let mut s = self.state.lock(); + let enabled = s.r[21 + port] & 2; + s.ports[port] = None; + s.low_speed[port] = false; + s.reset_ms[port] = 0; + s.r[21 + port] = + (s.r[21 + port] & PPS) | 0x10000 | if enabled != 0 { 0x20000 } else { 0 }; + s.r[3] |= RHSC; + } + self.drive_irq(); + Ok(()) + } + /// Deterministic millisecond ticks; `sync_clock` delivers the same ticks from the clock. + pub fn advance_frames(&self, count: u64) { + for _ in 0..count { + if !self.running.load(Ordering::Acquire) { + break; + } + { + let _pass = self.pass.lock(); + if !self.running.load(Ordering::Acquire) { + break; + } + let mut s = self.state.lock().clone(); + self.frame(&mut s); + *self.state.lock() = s; + } + self.drive_irq(); + } + } + fn frame(&self, s: &mut State) { + s.now_ms = s.now_ms.wrapping_add(1); + for p in 0..2 { + if s.reset_ms[p] != 0 { + s.reset_ms[p] -= 1; + if s.reset_ms[p] == 0 { + s.r[21 + p] = (s.r[21 + p] & !16) | 2 | (1 << 20); + s.r[3] |= RHSC; + } + } + } + if s.r[1] & 0xc0 != 0x80 || s.r[3] & UE != 0 { + return; + } + s.bit_credit += 12000; + while s.bit_credit >= u64::from((s.active_fi & 0x3fff) + 1) { + s.bit_credit -= u64::from((s.active_fi & 0x3fff) + 1); + s.active_fi = s.r[13]; + if self.schedule(s).is_err() { + s.r[3] |= UE; + s.bit_credit = 0; + self.config.set_status_bits(0x8000); + break; + } + } + } +} +impl PciBusDevice for Ohci { + fn config(&self) -> &PciConfig { + &self.config + } + fn connect_dma(&self, dma: Arc) { + *self.dma.lock() = Some(dma); + } + fn connect_interrupt(&self, line: IrqLine) { + *self.irq.lock() = line; + self.drive_irq(); + } + fn bar_read(&self, bar: usize, off: u32, width: usize) -> u32 { + if bar != 0 || width != 4 || off & 3 != 0 || off > 0x58 { + crate::dlog!( + crate::devlog::LogModule::Usb, + "ohci unsupported read at register offset {off:#x}, width {width}" + ); + return 0; + } + self.sync_clock(); + let s = self.state.lock(); + let i = (off / 4) as usize; + if i == 5 { + s.r[4] + } else if i == 21 || i == 22 { + s.r[i] | if s.r[18] & 0x200 != 0 { PPS } else { 0 } + } else { + s.r[i] + } + } + fn bar_write(&self, bar: usize, off: u32, val: u32, width: usize) { + if bar != 0 || width != 4 || off & 3 != 0 || off > 0x58 { + crate::dlog!( + crate::devlog::LogModule::Usb, + "ohci unsupported write at register offset {off:#x}, width {width}" + ); + return; + } + self.sync_clock(); + // Taken before `pass`, as `sync_clock` orders them. Only a control + // write can restart the frame phase. + let mut anchor = (off == 4).then(|| self.clock.lock()); + let mut reset = None; + { + let _pass = self.pass.lock(); + let mut s = self.state.lock(); + match off / 4 { + 1 => { + let old = s.r[1]; + s.r[1] = val & 0x7ff; + if val & 0xc0 == 0x80 && old & 0xc0 != 0x80 { + // OpenHCI 1.0a 6.2.1, 7.3.2, 7.3.3: entering + // UsbOperational loads HcFmRemaining from the frame + // interval and increments HcFmNumber, with no SOF at + // the load; the first follows one frame later. The + // free-running clock's phase therefore restarts here. + s.bit_credit = 0; + s.active_fi = s.r[13]; + s.r[14] = (s.r[13] & 0x8000_0000) | (s.r[13] & 0x3fff); + let old_frame = s.r[15] as u16; + let frame = old_frame.wrapping_add(1); + s.r[15] = u32::from(frame); + if (old_frame ^ frame) & 0x8000 != 0 { + s.r[3] |= 32; + } + if let Some(Some(at)) = anchor.as_deref_mut() { + *at = self.now(); + } + } + if val & 0xc0 == 0 && old & 0xc0 != 0 { + s.r[18] = 0x10000902; + for p in 0..2 { + s.power(p, false); + } + } + if val & 0xc0 == 0x40 || val & 0xc0 == 0xc0 { + crate::dlog!( + crate::devlog::LogModule::Usb, + "ohci suspend/resume timing unsupported at control register offset 0x04" + ); + } + } + 2 => { + if val & 1 != 0 { + s.soft_reset(); + } else { + s.r[2] |= val & 6; + if val & 8 != 0 { + s.r[3] |= 1 << 30; + } + } + } + 3 => s.r[3] &= !(val & EVENTS), + 4 => s.r[4] |= val & (EVENTS | 0x80000000), + 5 => s.r[4] &= !(val & (EVENTS | 0x80000000)), + 6 => s.r[6] = val & !255, + 7..=11 => s.r[(off / 4) as usize] = val & !15, + 13 => s.r[13] = val & 0xffff3fff, + 16 => s.r[16] = val & 0x3fff, + 17 => s.r[17] = val & 0xfff, + 18 => { + s.r[18] = (s.r[18] & !0xff001b00) | (val & 0xff001b00); + if val & 0x200 != 0 { + for p in 0..2 { + s.power(p, true); + } + } + } + 19 => s.r[19] = val & 0x60006, + 20 => { + if val & (1 << 15) != 0 { + s.r[20] |= 1 << 15; + } + if val & (1 << 31) != 0 { + s.r[20] &= !(1 << 15); + } + if val & (1 << 17) != 0 { + s.r[20] &= !(1 << 17); + } + if s.r[18] & 0x200 == 0 { + for p in 0..2 { + if !s.individual(p) { + if val & 1 != 0 { + s.power(p, false); + } + if val & (1 << 16) != 0 { + s.power(p, true); + } + } + } + } + } + 21 | 22 => { + let p = (off / 4 - 21) as usize; + let i = 21 + p; + s.r[i] &= !(val & CHANGES); + if val & 1 != 0 { + s.r[i] &= !2; + } + if val & 2 != 0 { + if s.r[i] & 1 != 0 && s.powered(p) { + s.r[i] |= 2; + } else { + s.r[i] |= 1 << 16; + } + } + if val & 12 != 0 { + crate::dlog!( + crate::devlog::LogModule::Usb, + "ohci port suspend/resume unsupported at port register offset {:#x}", + 0x54 + p * 4 + ); + } + if val & 16 != 0 { + if s.r[i] & 1 != 0 && s.powered(p) { + s.r[i] |= 16; + s.r[i] &= !2; + s.reset_ms[p] = 10; + reset = s.ports[p].clone(); + } else { + s.r[i] |= 1 << 16; + } + } + if s.individual(p) && s.r[18] & 0x200 == 0 { + if val & PPS != 0 { + s.power(p, true); + } + if val & 0x200 != 0 { + s.power(p, false); + } + } + if s.r[i] & CHANGES != 0 { + s.r[3] |= RHSC; + } + } + _ => { + crate::dlog!( + crate::devlog::LogModule::Usb, + "ohci write to read-only register offset {off:#x}" + ); + } + } + } + drop(anchor); + if let Some(f) = reset { + f.reset(); + } + self.drive_irq(); + } +} +impl Resettable for Ohci { + fn power_on(&self) { + let ports = { + let _pass = self.pass.lock(); + let mut s = self.state.lock(); + let ports = s.ports.clone(); + let low_speed = s.low_speed; + *s = State::new(); + s.ports = ports.clone(); + s.low_speed = low_speed; + self.config.reset(); + ports + }; + for f in ports.into_iter().flatten() { + f.reset(); + } + self.drive_irq(); + } +} +impl Device for Ohci { + fn start(&self) { + if self.running.swap(true, Ordering::AcqRel) { + return; + } + let now = self.now(); + *self.clock.lock() = Some(now); + } + fn stop(&self) { + self.sync_clock(); + self.running.store(false, Ordering::Release); + *self.clock.lock() = None; + drop(self.pass.lock()); + } + fn is_running(&self) -> bool { + self.running.load(Ordering::Acquire) + } + fn step(&self, _: u64) {} + fn get_clock(&self) -> u64 { + 0 + } +} + +#[cfg(test)] +mod register_tests { + use super::*; + use crate::pci::PciBusDevice; + use crate::traits::Resettable; + + #[test] + fn lucent_identity_and_reset_contract() { + let hc = Ohci::new(); + assert_eq!(hc.config().read(0), 0x5802_11c1); + assert_eq!(hc.config().read(2), 0x0c03_1010); + assert_eq!(hc.config().read(11), 0xffff_ffff); + assert_eq!(hc.config().read(1), 0x0200_0000); + assert_eq!(hc.bar_read(0, 0, 4), 0x110); + assert_eq!(hc.bar_read(0, 0x10, 4), 0); + assert_eq!(hc.bar_read(0, 0x34, 4), 0x2edf); + assert_eq!(hc.bar_read(0, 0x48, 4), 0x1000_0902); + assert_eq!(hc.bar_read(0, 0x4c, 4), 0x0006_0000); + for p in [0x54, 0x58] { + assert_eq!(hc.bar_read(0, p, 4), 0); + } + hc.bar_write(0, 0x10, 0xffff_ffff, 4); + hc.bar_write(0, 0x08, 1, 4); + assert_eq!(hc.bar_read(0, 0x10, 4), 0); + assert_eq!(hc.bar_read(0, 0x04, 4) & 0xc0, 0xc0); + hc.power_on(); + assert_eq!(hc.bar_read(0, 0x04, 4), 0); + } +} + +#[cfg(test)] +#[path = "ohci/tests.rs"] +mod tests; diff --git a/src/dev/ohci/engine.rs b/src/dev/ohci/engine.rs new file mode 100644 index 000000000..7b76cc646 --- /dev/null +++ b/src/dev/ohci/engine.rs @@ -0,0 +1,223 @@ +//! General TD packet processing; guest DMA byte slices are the endianness edge. +use super::*; + +fn read_words(dma: &dyn PciDma, at: u32) -> Result<[u32; 4], ()> { + let mut bytes = [0; 16]; + if !dma.dma_read(at as u64, &mut bytes).is_ok() { + return Err(()); + } + Ok(std::array::from_fn(|i| { + u32::from_le_bytes(bytes[i * 4..i * 4 + 4].try_into().unwrap()) + })) +} +fn write_words(dma: &dyn PciDma, at: u32, words: &[u32]) -> Result<(), ()> { + let bytes: Vec = words.iter().flat_map(|x| x.to_le_bytes()).collect(); + if dma.dma_write(at as u64, &bytes).is_ok() { + Ok(()) + } else { + Err(()) + } +} +fn remaining(cbp: u32, be: u32) -> Result { + if cbp == 0 { + return Ok(0); + } + if cbp & !0xfff == be & !0xfff { + be.checked_sub(cbp).map(|n| n as usize + 1).ok_or(()) + } else { + Ok(0x1000 - (cbp as usize & 0xfff) + (be as usize & 0xfff) + 1) + } +} +fn advance(cbp: u32, be: u32, n: usize) -> u32 { + let first = 0x1000 - (cbp as usize & 0xfff); + if n < first { + cbp.wrapping_add(n as u32) + } else { + (be & !0xfff).wrapping_add((n - first) as u32) + } +} +fn payload_read(dma: &dyn PciDma, cbp: u32, be: u32, len: usize) -> Result, ()> { + let mut bytes = vec![0; len]; + let first = len.min(0x1000 - (cbp as usize & 0xfff)); + if first != 0 && !dma.dma_read(cbp as u64, &mut bytes[..first]).is_ok() { + return Err(()); + } + if len > first + && !dma + .dma_read((be & !0xfff) as u64, &mut bytes[first..]) + .is_ok() + { + return Err(()); + } + Ok(bytes) +} +fn payload_write(dma: &dyn PciDma, cbp: u32, be: u32, bytes: &[u8]) -> Result<(), ()> { + let first = bytes.len().min(0x1000 - (cbp as usize & 0xfff)); + if first != 0 && !dma.dma_write(cbp as u64, &bytes[..first]).is_ok() { + return Err(()); + } + if bytes.len() > first && !dma.dma_write((be & !0xfff) as u64, &bytes[first..]).is_ok() { + return Err(()); + } + Ok(()) +} +impl Ohci { + /// Returns the next ED and whether this ED still holds queued general work. + pub(super) fn endpoint( + &self, + s: &mut State, + dma: &dyn PciDma, + at: u32, + ) -> Result<(u32, bool, bool), ()> { + let mut ed = read_words(dma, at)?; + let next = ed[3] & !15; + if ed[0] & (1 << 15) != 0 { + crate::dlog!(crate::devlog::LogModule::Usb, "ohci isochronous ED unsupported at guest address {at:#x}"); + return Ok((next, false, false)); + } + if ed[0] & (1 << 14) != 0 || ed[2] & 1 != 0 || ed[2] & !15 == ed[1] & !15 { + return Ok((next, false, false)); + } + let td_at = ed[2] & !15; + let mut td = read_words(dma, td_at)?; + let mps = ((ed[0] >> 16) & 0x7ff) as usize; + if mps == 0 || mps > 1023 { + crate::dlog!(crate::devlog::LogModule::Usb, "ohci invalid maximum packet size {mps} at ED address {at:#x}"); + return Ok((next, false, false)); + } + let Ok(n) = remaining(td[1], td[3]) else { + crate::dlog!(crate::devlog::LogModule::Usb, "ohci malformed TD buffer range at ED address {at:#x}"); + return Ok((next, false, false)); + }; + let direction = match (ed[0] >> 11) & 3 { + 1 => 1, + 2 => 2, + _ => (td[0] >> 19) & 3, + }; + let pid = match direction { + 0 => Pid::Setup, + 1 => Pid::Out, + 2 => Pid::In, + _ => { + crate::dlog!(crate::devlog::LogModule::Usb, "ohci reserved PID at ED address {at:#x}"); + return Ok((next, false, false)); + } + }; + let length = n.min(mps); + let toggle = if td[0] & (1 << 25) != 0 { + td[0] & (1 << 24) != 0 + } else { + ed[2] & 2 != 0 + }; + let outgoing = if pid != Pid::In { + payload_read(dma, td[1], td[3], length)? + } else { + vec![] + }; + let address = (ed[0] & 127) as u8; + let mut functions = (0..2) + .filter(|&p| s.powered(p) && s.r[21 + p] & 0x16 == 2) + .filter_map(|p| s.ports[p].clone()) + .filter(|f| f.address() == address); + // Multiple matching functions are an unsupported topology. Do not choose + // a recipient by port order or claim to model electrical collisions. + let first = functions.next(); + let function = if functions.next().is_none() { + first + } else { + None + }; + let reply = if let Some(f) = function { + f.transact(UsbPacket { + endpoint: ((ed[0] >> 7) & 15) as u8, + pid, + toggle, + capacity: if pid == Pid::In { mps } else { length }, + data: outgoing, + time_ms: s.now_ms, + }) + } else { + UsbOutcome::NoResponse + }; + let mut retire = false; + let mut cc = 0; + let mut halt = false; + match reply { + UsbOutcome::Nak => return Ok((next, true, true)), + UsbOutcome::Stall => { + cc = 4; + halt = true; + retire = true; + } + UsbOutcome::NoResponse | UsbOutcome::CrcError | UsbOutcome::BitStuffError => { + cc = match reply { + UsbOutcome::CrcError => 1, + UsbOutcome::BitStuffError => 2, + _ => 5, + }; + let ec = ((td[0] >> 26) & 3) + 1; + td[0] = (td[0] & !(3 << 26)) | ((ec.min(3)) << 26); + if ec >= 3 { + retire = true; + halt = true; + } + } + UsbOutcome::Ack(bytes) => { + let actual = if pid == Pid::In { bytes.len() } else { length }; + if pid == Pid::In { + payload_write(dma, td[1], td[3], &bytes[..actual.min(n).min(mps)])?; + } + td[0] = (td[0] & !((3 << 24) | (3 << 26))) | ((2 | u32::from(!toggle)) << 24); + if actual > length { + cc = 8; + halt = true; + retire = true; + } else { + td[1] = if actual == n { + 0 + } else { + advance(td[1], td[3], actual) + }; + if actual < length { + retire = true; + if td[0] & (1 << 18) == 0 { + cc = 9; + halt = true; + } + } else if actual == n { + retire = true; + } + } + } + } + td[0] = (td[0] & 0x0fff_ffff) | (cc << 28); + if retire { + let next_td = td[2] & !15; + td[2] = s.r[12]; + // Failure stops immediately. Earlier successful writes and USB side + // effects remain visible; descriptor writeback is not a transaction. + ed[2] = next_td | ((td[0] >> 23) & 2) | u32::from(halt); + write_words(dma, at + 8, &[ed[2]])?; + write_words(dma, td_at, &td)?; + s.r[12] = td_at; + let di = ((td[0] >> 21) & 7) as u8; + s.done_delay = s.done_delay.min(if cc != 0 { 0 } else { di }); + Ok((next, next_td != ed[1] & !15 && !halt, true)) + } else { + write_words(dma, td_at, &td)?; + Ok((next, true, true)) + } + } + pub(super) fn publish(&self, s: &mut State, dma: &dyn PciDma) -> Result<(), ()> { + if s.done_delay == 0 && s.r[3] & WDH == 0 && s.r[12] != 0 { + let other = u32::from(s.r[3] & s.r[4] & EVENTS & !WDH != 0); + write_words(dma, s.r[6] + 0x84, &[s.r[12] | other])?; + s.r[12] = 0; + s.done_delay = 7; + s.r[3] |= WDH; + } else if s.done_delay > 0 && s.done_delay < 7 { + s.done_delay -= 1; + } + Ok(()) + } +} diff --git a/src/dev/ohci/schedule.rs b/src/dev/ohci/schedule.rs new file mode 100644 index 000000000..b6c8c6255 --- /dev/null +++ b/src/dev/ohci/schedule.rs @@ -0,0 +1,98 @@ +//! Functional scheduling approximation: 256 synthetic units per ED visit. +//! Costs deliberately do not represent wire time. See the module's limitations. +use super::*; +const VISIT: u32 = 256; +impl Ohci { + fn list_ready(s: &mut State, head: usize, current: usize, filled: u32) -> bool { + if s.r[current] == 0 && s.r[2] & filled != 0 { + s.r[current] = s.r[head]; + s.r[2] &= !filled; + } + s.r[current] != 0 + } + fn nonperiodic( + &self, + s: &mut State, + dma: &dyn PciDma, + budget: &mut u32, + cutoff: u32, + credit: &mut u32, + ) -> Result<(), ()> { + let mut yielded: Option = None; + while *budget >= VISIT && *budget > cutoff { + let control = s.r[1] & 16 != 0 && Self::list_ready(s, 8, 9, 2); + let bulk = s.r[1] & 32 != 0 && Self::list_ready(s, 10, 11, 4); + if !control && !bulk { + break; + } + let choose_control = control + && (!bulk + || match yielded { + Some(was_control) => !was_control, + None => *credit > 0, + }); + let (current, filled) = if choose_control { (9, 2) } else { (11, 4) }; + let (next, queued, attempt) = self.endpoint(s, dma, s.r[current])?; + *budget -= VISIT; + s.r[current] = next; + if queued { + s.r[2] |= filled; + } + yielded = if attempt { None } else { Some(choose_control) }; + if attempt { + if choose_control { + *credit = credit.saturating_sub(1); + } else { + *credit = (s.r[1] & 3) + 1; + } + } + } + Ok(()) + } + pub(super) fn schedule(&self, s: &mut State) -> Result<(), ()> { + let old = s.r[15] as u16; + let frame = old.wrapping_add(1); + s.r[15] = u32::from(frame); + if (old ^ frame) & 0x8000 != 0 { + s.r[3] |= 32; + } + s.r[3] |= 4; + let mut budget = (s.active_fi & 0x3fff) + 1; + s.r[14] = (s.active_fi & 0x80000000) | budget.min(0x3fff); + if self.config.command() & 4 == 0 { + return Ok(()); + } + let Some(dma) = self.dma.lock().clone() else { + return Err(()); + }; + let frame_bytes = [frame as u8, (frame >> 8) as u8, 0, 0]; + if !dma.dma_write((s.r[6] + 0x80) as u64, &frame_bytes).is_ok() { + return Err(()); + } + let mut credit = (s.r[1] & 3) + 1; + let cutoff = s.r[16].min(budget); + self.nonperiodic(s, &*dma, &mut budget, cutoff, &mut credit)?; + if s.r[1] & 4 != 0 { + let mut bytes = [0; 4]; + if !dma + .dma_read((s.r[6] + u32::from(frame & 31) * 4) as u64, &mut bytes) + .is_ok() + { + return Err(()); + } + s.r[7] = u32::from_le_bytes(bytes) & !15; + while s.r[7] != 0 && budget >= VISIT { + let (next, _, _) = self.endpoint(s, &*dma, s.r[7])?; + s.r[7] = next; + budget -= VISIT; + } + if s.r[7] != 0 { + s.r[3] |= 1; + s.r[2] = (s.r[2] & !0x30000) | ((s.r[2].wrapping_add(0x10000)) & 0x30000); + } + } + self.nonperiodic(s, &*dma, &mut budget, 0, &mut credit)?; + s.r[14] = (s.active_fi & 0x80000000) | budget.min(0x3fff); + self.publish(s, &*dma) + } +} diff --git a/src/dev/ohci/tests.rs b/src/dev/ohci/tests.rs new file mode 100644 index 000000000..1d0733cd3 --- /dev/null +++ b/src/dev/ohci/tests.rs @@ -0,0 +1,554 @@ +// The register and clock cases follow tests/qtest/usb-lucent-ohci-test.c in +// jamesbraid/qemu-sgi-origami: root-hub-descriptor-a-is-writable and +// frame-number-tracks-clock-while-timer-starved. The descriptor engine cases +// drive a test-only function and have no qtest counterpart. +use super::*; +use crate::pci::{PciBusDevice, PciDmaStatus}; +use crate::traits::{Device, Resettable}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Weak; + +struct Memory { + bytes: Mutex>, + refuse_read: Mutex>, + refuse_write: Mutex>, + writes: Mutex>, +} +impl Memory { + fn new() -> Arc { + Arc::new(Self { + bytes: Mutex::new(vec![0xa5; 0x10000]), + refuse_read: Mutex::new(None), + refuse_write: Mutex::new(None), + writes: Mutex::new(vec![]), + }) + } + fn words(&self, at: u32, words: &[u32]) { + for (i, w) in words.iter().enumerate() { + self.bytes.lock()[at as usize + i * 4..at as usize + i * 4 + 4] + .copy_from_slice(&w.to_le_bytes()); + } + } + fn word(&self, at: u32) -> u32 { + u32::from_le_bytes( + self.bytes.lock()[at as usize..at as usize + 4] + .try_into() + .unwrap(), + ) + } +} +impl PciDma for Memory { + fn dma_read(&self, addr: u64, dst: &mut [u8]) -> PciDmaStatus { + if self + .refuse_read + .lock() + .is_some_and(|x| (addr..addr + dst.len() as u64).contains(&(x as u64))) + { + return PciDmaStatus::MasterAbort; + } + let b = self.bytes.lock(); + let Some(s) = b.get(addr as usize..addr as usize + dst.len()) else { + return PciDmaStatus::MasterAbort; + }; + dst.copy_from_slice(s); + PciDmaStatus::Ok + } + fn dma_write(&self, addr: u64, src: &[u8]) -> PciDmaStatus { + if self + .refuse_write + .lock() + .is_some_and(|x| (addr..addr + src.len() as u64).contains(&(x as u64))) + { + return PciDmaStatus::MasterAbort; + } + let mut b = self.bytes.lock(); + let Some(s) = b.get_mut(addr as usize..addr as usize + src.len()) else { + return PciDmaStatus::MasterAbort; + }; + s.copy_from_slice(src); + self.writes.lock().push(addr); + PciDmaStatus::Ok + } +} +struct Function { + address: AtomicUsize, + calls: AtomicUsize, + reply: Mutex, + packets: Mutex>, +} +impl Function { + fn new(reply: UsbOutcome) -> Arc { + Arc::new(Self { + address: AtomicUsize::new(0), + calls: AtomicUsize::new(0), + reply: Mutex::new(reply), + packets: Mutex::new(vec![]), + }) + } +} +impl UsbFunction for Function { + fn reset(&self) {} + fn address(&self) -> u8 { + self.address.load(Ordering::SeqCst) as u8 + } + fn transact(&self, packet: UsbPacket) -> UsbOutcome { + self.calls.fetch_add(1, Ordering::SeqCst); + self.packets.lock().push(packet); + self.reply.lock().clone() + } +} +fn start_port_reset(h: &Ohci, port: u32) { + let status = 0x54 + port * 4; + h.bar_write(0, status, 0x100, 4); + h.bar_write(0, status, 0x10, 4); +} + +fn setup( + pid: Pid, + reply: UsbOutcome, + cbp: u32, + be: u32, + flags: u32, +) -> (Arc, Arc, Arc) { + let h = Ohci::new(); + let m = Memory::new(); + let f = Function::new(reply); + h.connect_dma(m.clone()); + h.attach(0, f.clone()).unwrap(); + h.start(); + start_port_reset(&h, 0); + h.advance_frames(10); + m.words(0x1000, &[0; 64]); + m.words(0x2000, &[(8 << 16), 0x3020, 0x3000, 0]); + m.words( + 0x3000, + &[0xf000_0000 | flags | ((pid as u32) << 19), cbp, 0x3020, be], + ); + h.config().write(1, 6); + h.bar_write(0, 0x18, 0x1000, 4); + h.bar_write(0, 0x20, 0x2000, 4); + h.bar_write(0, 0x08, 2, 4); + h.bar_write(0, 0x04, 0x90, 4); + (h, m, f) +} +/// The guest receives fatal status if the controller cannot access a descriptor. +#[test] +fn dma_refusal_preserves_descriptors_and_reports_a_fatal_error() { + for periodic in [false, true] { + for pid in [Pid::Out, Pid::In] { + let (h, m, f) = setup(pid, UsbOutcome::Ack(vec![5; 8]), 0x4000, 0x4007, 0); + if periodic { + m.words(0x1000, &[0x2000; 32]); + h.bar_write(0, 4, 0x84, 4); + } + if pid == Pid::In { + *m.refuse_write.lock() = Some(0x4000); + } else { + *m.refuse_read.lock() = Some(0x4000); + } + let before = m.bytes.lock()[0x3000..0x3010].to_vec(); + h.advance_frames(1); + let frame = h.bar_read(0, 0x3c, 4); + h.advance_frames(3); + assert_eq!(f.calls.load(Ordering::SeqCst), usize::from(pid == Pid::In)); + assert_eq!(&m.bytes.lock()[0x3000..0x3010], &before); + assert_eq!(m.word(0x2008), 0x3000); + assert_eq!(m.word(0x1084), 0); + assert_eq!(h.bar_read(0, 0x3c, 4), frame); + assert_ne!(h.bar_read(0, 0xc, 4) & UE, 0); + assert_eq!(h.config().read(1) >> 16, 0x8200); + h.config().write(1, 0x8000_0006); + assert_eq!(h.config().read(1) >> 16, 0x0200); + assert_ne!(h.bar_read(0, 0xc, 4) & UE, 0); + } + } +} + +/// Short and overlong USB replies leave bytes outside the packet untouched. +#[test] +fn short_or_overlong_in_packets_preserve_unwritten_bytes() { + for (name, payload, end, flags, bytes, current, condition) in [ + ("short", vec![1, 2, 3], 0x401f, (1 << 18) | (3 << 24), [1, 2, 3, 0xa5, 0xa5], 0x4003, 0), + ("overrun", vec![1; 8], 0x4003, 3 << 24, [1, 1, 1, 1, 0xa5], 0x4000, 8), + ] { + let (h, m, _) = setup(Pid::In, UsbOutcome::Ack(payload), 0x4000, end, flags); + h.advance_frames(1); + assert_eq!(&m.bytes.lock()[0x4000..0x4005], &bytes, "{name}"); + assert_eq!(m.word(0x3004), current, "{name}"); + assert_eq!(m.word(0x3000) >> 28, condition, "{name}"); + assert_eq!((m.word(0x3000) >> 24) & 3, 2, "{name}"); + if condition == 0 { + assert_eq!(m.word(0x2008) & !15, 0x3020); + assert_eq!(m.word(0x1084) & !15, 0x3000); + } + } +} + +/// The guest retries NAKs, stops on STALL, and retains consistent short-transfer state. +#[test] +fn nak_stall_and_short_in_completions_keep_the_endpoint_consistent() { + for (name, reply, flags, condition) in [ + ("NAK", UsbOutcome::Nak, 0, 15), + ("STALL", UsbOutcome::Stall, 3 << 24, 4), + ("no response", UsbOutcome::NoResponse, 0, 5), + ("short without rounding", UsbOutcome::Ack(vec![2]), 0, 9), + ] { + let (h, m, f) = setup(Pid::In, reply, 0x4000, 0x4007, flags); + h.bar_write(0, 0x34, 255, 4); + let before = m.bytes.lock()[0x3000..0x3010].to_vec(); + h.advance_frames(1); + if condition == 5 { + assert_eq!(f.calls.load(Ordering::SeqCst), 1); + assert_eq!((m.word(0x3000) >> 26) & 3, 1); + assert_eq!(m.word(0x2008), 0x3000); + h.advance_frames(2); + } + assert_eq!(m.word(0x3000) >> 28, condition, "{name}"); + if condition == 15 { + assert_eq!(&m.bytes.lock()[0x3000..0x3010], &before); + assert_eq!(m.word(0x2008), 0x3000); + } else { + assert_ne!(m.word(0x2008) & 1, 0, "{name}"); + } + if condition == 4 { + assert_eq!(m.word(0x3004), 0x4000); + assert_eq!((m.word(0x3000) >> 24) & 3, 3); + assert_eq!(m.word(0x2008) & 3, 3); + } + } +} + +/// Port and controller reset paths retain attached USB functions. +#[test] +fn attached_functions_survive_port_and_controller_resets() { + let h = Ohci::new(); + h.start(); + h.attach(1, Function::new(UsbOutcome::Nak)).unwrap(); + assert_eq!(h.bar_read(0, 0x58, 4) & 1, 0); + start_port_reset(&h, 1); + h.advance_frames(9); + assert_ne!(h.bar_read(0, 0x58, 4) & 16, 0); + h.advance_frames(1); + assert_eq!(h.bar_read(0, 0x58, 4) & 16, 0); + assert_ne!(h.bar_read(0, 0x58, 4) & (1 << 20), 0); + h.bar_write(0, 8, 1, 4); + assert_ne!(h.bar_read(0, 0x58, 4) & 1, 0); + h.bar_write(0, 0x58, 0x200, 4); + assert_eq!(h.bar_read(0, 0x58, 4) & 0x117, 0); + h.bar_write(0, 0x58, PPS, 4); + assert_ne!(h.bar_read(0, 0x58, 4) & 1, 0); + h.power_on(); + assert_eq!(h.bar_read(0, 0x58, 4), 0); + h.bar_write(0, 0x58, PPS, 4); + assert_ne!(h.bar_read(0, 0x58, 4) & 1, 0); +} + +/// The guest observes descriptor updates before the done bit and acknowledges them later. +#[test] +fn done_publication_orders_writes_and_waits_for_acknowledgement() { + let (h, m, _) = setup(Pid::In, UsbOutcome::Ack(vec![8; 8]), 0x4000, 0x4007, 0); + h.advance_frames(1); + { + let writes = m.writes.lock(); + assert!(writes.iter().position(|a| *a == 0x2008).unwrap() < writes.iter().position(|a| *a == 0x3000).unwrap()); + } + assert_eq!(m.word(0x1084) & !15, 0x3000); + m.words(0x3020, &[0xf0000000 | (2 << 19), 0x4100, 0x3040, 0x4107]); + m.words(0x2004, &[0x3040]); + h.bar_write(0, 8, 2, 4); + h.advance_frames(1); + assert_eq!(m.word(0x1084) & !15, 0x3000); + assert_eq!(h.bar_read(0, 0x30, 4), 0x3020); + h.bar_write(0, 0xc, 2, 4); + h.advance_frames(1); + assert_eq!(m.word(0x1084) & !15, 0x3020); + for delay in [0, 2, 7] { + let (h, m, _) = setup(Pid::In, UsbOutcome::Ack(vec![5; 8]), 0x4000, 0x4007, delay << 21); + h.advance_frames(1); + if delay != 0 { assert_eq!(m.word(0x1084), 0); } + h.advance_frames(u64::from(delay)); + assert_eq!(m.word(0x1084) & !15, if delay == 7 { 0 } else { 0x3000 }); + } + let (h, m, _) = setup(Pid::In, UsbOutcome::Ack(vec![1; 8]), 0x4000, 0x4007, 0); + *m.refuse_write.lock() = Some(0x1084); + h.advance_frames(1); + assert_ne!(h.bar_read(0, 0xc, 4) & UE, 0); + assert_eq!(h.bar_read(0, 0xc, 4) & WDH, 0); + assert_eq!(m.word(0x1084), 0); +} + +/// OHCI must yield to the other transfer list when a guest marks one list skipped. +#[test] +fn scheduler_shares_service_and_yields_past_skipped_lists() { + for (cbsr, skip_control, skip_bulk) in [(0, false, false), (3, false, false), (3, true, false), (0, false, true)] { + let (h, m, f) = setup(Pid::In, UsbOutcome::Nak, 0x4000, 0x4007, 0); + m.words(0x2000, &[(8 << 16) | (1 << 7) | (u32::from(skip_control) << 14), 0x3020, 0x3000, if skip_control { 0x2000 } else { 0 }]); + m.words(0x2100, &[(8 << 16) | (2 << 7) | (u32::from(skip_bulk) << 14), 0x3120, 0x3100, if skip_bulk { 0x2100 } else { 0 }]); + m.words(0x3100, &[0xf0100000, 0x4100, 0x3120, 0x4107]); + h.bar_write(0, 0x28, 0x2100, 4); + h.bar_write(0, 8, 6, 4); + h.bar_write(0, 4, 0xb0 | cbsr, 4); + h.advance_frames(1); + let packets = f.packets.lock(); + let control = packets.iter().filter(|p| p.endpoint == 1).count(); + let bulk = packets.iter().filter(|p| p.endpoint == 2).count(); + assert_eq!(control > 0, !skip_control); + assert_eq!(bulk > 0, !skip_bulk); + if skip_bulk { + assert!(control > 1, "control resumes after its credit is exhausted"); + } + if !skip_control && !skip_bulk { + assert!(control >= bulk * (cbsr as usize + 1)); + assert!(control <= bulk * (cbsr as usize + 1) + cbsr as usize + 1); + } + assert_eq!(h.bar_read(0, 0xc, 4) & UE, 0); + } +} + +/// The controller routes address zero only while one attached function can claim it. +#[test] +fn address_zero_is_routed_only_to_a_unique_eligible_function() { + for (name, address, peer_status, first_selected, peer_selected) in [ + ("ambiguous", 0, PPS | 1 | 2, false, false), + ("disabled peer", 0, PPS | 1, true, false), + ("assigned first", 5, PPS | 1 | 2, false, true), + ] { + let (h, m, f) = setup(Pid::In, UsbOutcome::Nak, 0x4000, 0x4007, 0); + let peer = Function::new(UsbOutcome::Nak); + h.attach(1, peer.clone()).unwrap(); + h.state.lock().r[22] = peer_status; + f.address.store(address, Ordering::SeqCst); + h.advance_frames(1); + assert_eq!(f.calls.load(Ordering::SeqCst) > 0, first_selected, "{name}"); + assert_eq!(peer.calls.load(Ordering::SeqCst) > 0, peer_selected, "{name}"); + if address != 0 { + let peer_calls = peer.calls.load(Ordering::SeqCst); + m.words(0x2000, &[(8 << 16) | 5]); + h.advance_frames(1); + assert!(f.calls.load(Ordering::SeqCst) > 0); + assert_eq!(peer.calls.load(Ordering::SeqCst), peer_calls); + } else if !first_selected { + assert_eq!(m.word(0x3000) >> 28, 5); + assert_eq!((m.word(0x3000) >> 26) & 3, 3); + } + } +} + + +/// A guest USB packet can cross physically nonadjacent DMA pages. +#[test] +fn packet_crosses_nonadjacent_pages() { + let (h, m, _) = setup( + Pid::In, + UsbOutcome::Ack(vec![1, 2, 3, 4, 5, 6, 7, 8]), + 0x4ffc, + 0x9003, + 0, + ); + h.advance_frames(1); + assert_eq!(&m.bytes.lock()[0x4ffc..0x5000], &[1, 2, 3, 4]); + assert_eq!(&m.bytes.lock()[0x9000..0x9004], &[5, 6, 7, 8]); + assert_eq!(m.bytes.lock()[0x5000], 0xa5); + assert_eq!(m.word(0x3004), 0); +} + +/// The frame counter catches up after its host timer was delayed. +#[test] +fn clock_read_catches_up_after_timer_starvation() { + let h = Ohci::new(); + let now = Arc::new(AtomicUsize::new(0)); + let t = now.clone(); + h.set_clock(Arc::new(move || t.load(Ordering::Relaxed) as u64)); + h.start(); + h.bar_write(0, 4, 0x80, 4); + now.store(5_000_000, Ordering::Relaxed); + assert!(h.bar_read(0, 0x3c, 4) >= 5); + h.stop(); + let frame = h.bar_read(0, 0x3c, 4); + h.advance_frames(10); + assert_eq!(h.bar_read(0, 0x3c, 4), frame); +} + +/// The frame count follows the guest's clock, not when the machine's poll or +/// a timer thread last ran: a guest that waits 1 ms after going operational +/// and reads HcFmNumber sees the frame, even if nothing else advanced the +/// controller in between (a slow or descheduled host thread). +#[test] +fn frame_number_follows_the_clock_with_no_poll_or_timer() { + let h = Ohci::new(); + let now = Arc::new(AtomicUsize::new(10_000_000)); + let t = now.clone(); + h.set_clock(Arc::new(move || t.load(Ordering::Relaxed) as u64)); + h.start(); + // Part-way into a frame period when the guest sets operational. + now.store(10_400_000, Ordering::Relaxed); + h.bar_write(0, 4, 0x80, 4); + // Entering Operational increments the frame number at once. + assert_eq!(h.bar_read(0, 0x3c, 4), 1); + // 1.1 ms on the guest's clock: the first boundary, 1 ms after the write, has passed. + now.store(11_500_000, Ordering::Relaxed); + assert_eq!(h.bar_read(0, 0x3c, 4), 2); + // A long guest-time gap is delivered in full at the next access. + now.store(31_500_000, Ordering::Relaxed); + assert_eq!(h.bar_read(0, 0x3c, 4), 22); + // Time that has not passed on the guest's clock delivers nothing. + assert_eq!(h.bar_read(0, 0x3c, 4), 22); + h.stop(); +} + +fn fake_clock_ohci(start_ns: usize) -> (Arc, Arc) { + let h = Ohci::new(); + let now = Arc::new(AtomicUsize::new(start_ns)); + let t = now.clone(); + h.set_clock(Arc::new(move || t.load(Ordering::Relaxed) as u64)); + h.start(); + (h, now) +} + +/// OpenHCI 1.0a sections 6.2.1 and 7.3.3: entering UsbOperational loads +/// HcFmRemaining from the frame interval, increments HcFmNumber, and sends the +/// first SOF one frame later, whatever the phase of the free-running clock. +fn operational_entry_restarts_the_frame(from_hcfs: Option) { + let (h, now) = fake_clock_ohci(10_000_000); + let m = Memory::new(); + h.connect_dma(m.clone()); + h.config().write(1, 6); + h.bar_write(0, 0x18, 0x1000, 4); + // Not operational: no frames count however much time passes. + if let Some(hcfs) = from_hcfs { + h.bar_write(0, 4, hcfs, 4); + } + now.store(15_000_000, Ordering::Relaxed); + assert_eq!(h.bar_read(0, 0x3c, 4), 0); + now.store(15_400_000, Ordering::Relaxed); + h.bar_write(0, 4, 0x80, 4); + assert_eq!(h.bar_read(0, 0x3c, 4), 1); + assert_eq!(h.bar_read(0, 0x38, 4), 0x2edf); + assert_eq!(h.bar_read(0, 0x0c, 4) & 4, 0, "no SOF at the load"); + assert_eq!(m.bytes.lock()[0x1080], 0xa5, "no HCCA write at the load"); + // Just short of one frame after the write: still nothing. + now.store(16_399_999, Ordering::Relaxed); + assert_eq!(h.bar_read(0, 0x3c, 4), 1); + assert_eq!(h.bar_read(0, 0x0c, 4) & 4, 0); + assert_eq!(m.bytes.lock()[0x1080], 0xa5); + // Exactly one frame after: the first SOF and its HCCA frame number. + now.store(16_400_000, Ordering::Relaxed); + assert_eq!(h.bar_read(0, 0x0c, 4) & 4, 4); + assert_eq!(h.bar_read(0, 0x3c, 4), 2); + assert_eq!(m.bytes.lock()[0x1080], 2); + h.stop(); +} + +#[test] +fn operational_entry_from_reset_restarts_the_frame() { + operational_entry_restarts_the_frame(None); +} + +#[test] +fn operational_entry_from_suspend_restarts_the_frame() { + // HCFS 11b. + operational_entry_restarts_the_frame(Some(0xc0)); +} + +#[test] +fn operational_entry_from_resume_restarts_the_frame() { + // HCFS 01b. + operational_entry_restarts_the_frame(Some(0x40)); +} + +/// The increment on entry carries into the frame-number overflow flag like a +/// frame's own, and HcFmRemaining takes the frame-interval toggle bit too. +#[test] +fn operational_entry_sets_overflow_and_loads_the_interval_toggle() { + for (before, after, overflow) in [(0x7fff, 0x8000, true), (0xffff, 0, true), (0x1234, 0x1235, false)] { + let (h, _now) = fake_clock_ohci(10_000_000); + h.state.lock().r[15] = before; + h.bar_write(0, 0x34, 0x8000_2edf, 4); + h.bar_write(0, 4, 0x80, 4); + assert_eq!(h.bar_read(0, 0x3c, 4), after, "from {before:#x}"); + assert_eq!(h.bar_read(0, 0x0c, 4) & 32 != 0, overflow, "from {before:#x}"); + assert_eq!(h.bar_read(0, 0x38, 4), 0x8000_2edf); + h.stop(); + } +} + +/// An interrupt handler can acknowledge a line while the controller raises it. +#[test] +fn reentrant_irq_acknowledgement_leaves_final_line_clear() { + struct Ack { + hc: Weak, + level: AtomicBool, + once: AtomicBool, + } + impl crate::irq::InterruptController for Ack { + fn set_line(&self, _: u32, level: bool) { + if level && !self.once.swap(true, Ordering::SeqCst) { + self.hc.upgrade().unwrap().bar_write(0, 0xc, RHSC, 4); + } + self.level.store(level, Ordering::SeqCst); + } + } + let h = Ohci::new(); + let ack = Arc::new(Ack { + hc: Arc::downgrade(&h), + level: AtomicBool::new(false), + once: AtomicBool::new(false), + }); + h.connect_interrupt(IrqLine::new(ack.clone(), 0)); + h.bar_write(0, 0x10, 0x80000040, 4); + h.attach(0, Function::new(UsbOutcome::Nak)).unwrap(); + assert!(!ack.level.load(Ordering::SeqCst)); +} + +/// The guest sees the documented write masks, HCR exemptions, and ownership bit. +#[test] +fn register_masks_preserve_hcr_exempt_fields_and_ownership_status() { + let h = Ohci::new(); + h.bar_write(0, 0x18, 0x123456ff, 4); + assert_eq!(h.bar_read(0, 0x18, 4), 0x12345600); + h.bar_write(0, 0x20, 0x123456ff, 4); + assert_eq!(h.bar_read(0, 0x20, 4), 0x123456f0); + h.bar_write(0, 0x10, u32::MAX, 4); + assert_eq!(h.bar_read(0, 0x10, 4), 0xc000007f); + h.bar_write(0, 8, 8, 4); + assert_eq!(h.bar_read(0, 0xc, 4), 1 << 30); + h.bar_write(0, 0xc, 1 << 31, 4); + assert_eq!(h.bar_read(0, 0xc, 4), 1 << 30); + h.bar_write(0, 0xc, 1 << 30, 4); + assert_eq!(h.bar_read(0, 0xc, 4), 0); + h.bar_write(0, 0x48, u32::MAX, 4); + assert_eq!(h.bar_read(0, 0x48, 4), 0xff001b02); + h.bar_write(0, 4, 0x700, 4); + h.bar_write(0, 8, 1, 4); + assert_eq!(h.bar_read(0, 4, 4), 0x3c0); + assert_eq!(h.bar_read(0, 0x48, 4), 0xff001b02); + assert_eq!(h.bar_read(0, 0x10, 4), 0); + h.bar_write(0, 0x3c, 0xffff, 4); + assert_eq!(h.bar_read(0, 0x3c, 4), 0); + h.bar_write(0, 0x18, 1, 1); + assert_eq!(h.bar_read(0, 0x18, 4), 0); +} + +/// The periodic phase controls how the guest control and bulk lists share service. +#[test] +fn periodic_phase_preserves_control_and_bulk_credit() { + let (h, m, f) = setup(Pid::In, UsbOutcome::Nak, 0x4000, 0x4007, 0); + m.words(0x2000, &[(8 << 16) | (1 << 7), 0x3020, 0x3000, 0]); + m.words(0x2100, &[(8 << 16) | (2 << 7), 0x3120, 0x3100, 0]); + m.words(0x3100, &[0xf0100000, 0x4100, 0x3120, 0x4107]); + m.words(0x2200, &[(8 << 16) | (3 << 7), 0x3220, 0x3200, 0]); + m.words(0x3200, &[0xf0100000, 0x4200, 0x3220, 0x4207]); + for i in 0..32 { + m.words(0x1000 + i * 4, &[0x2200]); + } + let mut s = h.state.lock().clone(); + s.active_fi = 5 * 256 - 1; + s.r[16] = 3 * 256; + s.r[1] = 0xb5; + s.r[10] = 0x2100; + s.r[2] |= 4; + h.schedule(&mut s).unwrap(); + let order: Vec<_> = f.packets.lock().iter().map(|p| p.endpoint).collect(); + assert_eq!(order, [1, 1, 3, 2, 1]); + assert_eq!(s.r[14], 0); +} diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index fa24f7b6e..068e70084 100644 --- a/src/dev/ps2.rs +++ b/src/dev/ps2.rs @@ -57,8 +57,25 @@ struct Ps2State { const MOUSE_MAX: i32 = 127; /// Combined PS/2 Keyboard and Mouse Controller +/// Another input device on the same host keyboard and mouse: a machine whose +/// guest may use USB instead of PS/2 (the Fuel) fits one beside the 8042 or +/// IOC3 controller, and host input reaches both. Each device drops what its +/// guest has not enabled, as the PS/2 side does. +pub trait HostInputSink: Send + Sync { + fn key(&self, key: KeyCode, pressed: bool); + /// Buttons in PS/2 order (left 1, right 2, middle 4); `dy` is positive + /// down the screen. + fn mouse(&self, buttons: u8, dx: i32, dy: i32, dz: i32); + /// Whether the guest has enabled this mouse. + fn mouse_enabled(&self) -> bool; + /// Mouse movement queued that the guest has not read yet. + fn mouse_pending(&self) -> usize; +} + pub struct Ps2Controller { state: Mutex, + /// Other devices fed from the same host input. + sinks: Mutex>>, /// The controller's interrupt: up while a byte waits to be read. irq: Option, running: AtomicBool, @@ -91,6 +108,7 @@ impl Ps2Controller { held_keys: [false; 256], }), irq, + sinks: Mutex::new(Vec::new()), running: AtomicBool::new(false), layout: AtomicU8::new(KeyboardLayout::Ansi as u8), } @@ -526,17 +544,27 @@ impl Ps2Controller { /// clicks are dropped. pub fn mouse_enabled(&self) -> bool { let state = self.state.lock(); - state.mouse_enabled && state.config & 0x20 == 0 + (state.mouse_enabled && state.config & 0x20 == 0) + || self.sinks.lock().iter().any(|s| s.mouse_enabled()) + } + + /// Feed host keyboard and mouse input to `sink` as well. + pub fn add_sink(&self, sink: Arc) { + self.sinks.lock().push(sink); } /// Mouse bytes queued that the guest has not read yet. pub fn mouse_pending(&self) -> usize { - self.state.lock().mouse_queue_bytes + let own = self.state.lock().mouse_queue_bytes; + self.sinks.lock().iter().fold(own, |n, s| n.max(s.mouse_pending())) } /// Push a scancode byte to the keyboard queue (called from UI) pub fn push_kb(&self, key: KeyCode, pressed: bool) { if !self.running.load(Ordering::Relaxed) { return; } + for sink in self.sinks.lock().iter() { + sink.key(key, pressed); + } let mut state = self.state.lock(); if !state.scanning_enabled || state.config & 0x10 != 0 { return; @@ -968,6 +996,9 @@ impl Ps2Controller { /// - `dz`: scroll wheel delta (positive = scroll up/away from user) pub fn push_mouse_input(&self, buttons: u8, dx: i32, dy: i32, dz: i32) { if !self.running.load(Ordering::Relaxed) { return; } + for sink in self.sinks.lock().iter() { + sink.mouse(buttons, dx, dy, dz); + } let mut state = self.state.lock(); if !state.mouse_enabled || state.config & 0x20 != 0 { return; } @@ -1348,6 +1379,41 @@ impl Saveable for Ps2Controller { mod tests { use super::*; + /// A sink sees host keys and mouse moves even while the guest has not + /// enabled the PS/2 devices, and its mouse counts toward `mouse_enabled` + /// and `mouse_pending`. + #[test] + fn a_sink_shares_the_host_input() { + #[derive(Default)] + struct Sink(Mutex>); + impl HostInputSink for Sink { + fn key(&self, key: KeyCode, pressed: bool) { + self.0.lock().push(format!("{key:?} {pressed}")); + } + fn mouse(&self, buttons: u8, dx: i32, dy: i32, dz: i32) { + self.0.lock().push(format!("mouse {buttons} {dx} {dy} {dz}")); + } + fn mouse_enabled(&self) -> bool { + true + } + fn mouse_pending(&self) -> usize { + 5 + } + } + let p = Ps2Controller::new(None); + let sink = Arc::new(Sink::default()); + p.add_sink(sink.clone()); + p.push_kb(KeyCode::KeyA, true); + assert!(sink.0.lock().is_empty(), "a stopped controller passes nothing on"); + p.start(); + assert!(p.mouse_enabled(), "the sink's mouse is on though the PS/2 mouse is not"); + assert_eq!(p.mouse_pending(), 5); + p.push_kb(KeyCode::KeyA, true); + p.push_mouse_input(2, 3, -4, 1); + assert_eq!(*sink.0.lock(), ["KeyA true", "mouse 2 3 -4 1"]); + assert_eq!(p.pop_from(false), None, "and the PS/2 keyboard, not enabled, took nothing"); + } + /// `type_char` sends what the keys themselves would: Shift around a /// shifted character, Enter for a newline, and nothing for a character /// with no key. diff --git a/src/dev/usb_hid.rs b/src/dev/usb_hid.rs new file mode 100644 index 000000000..8f417c05b --- /dev/null +++ b/src/dev/usb_hid.rs @@ -0,0 +1,609 @@ +//! USB HID boot-protocol keyboard and mouse: low-speed functions that plug +//! into an OHCI root port and are fed from the host's keyboard and mouse. +//! +//! Written from the public specifications: USB 1.1 (chapter 9 requests and +//! descriptors), Device Class Definition for HID 1.11 (class requests and +//! Appendix B, the boot interface descriptors and report formats) and the HID +//! Usage Tables (keyboard/keypad page). Each device has one configuration, +//! one boot interface and one interrupt-IN endpoint. +//! +//! The control pipe follows USB 1.1 section 8.5.2: a SETUP packet, an optional +//! data stage and a status stage. SET_ADDRESS takes effect once its status +//! stage completes. Interrupt reports queue one per host-side change so a key +//! tapped and released between two polls is still seen twice. Input that +//! arrives before the guest has selected a configuration is dropped, as an +//! unconfigured device would not report it. +//! +//! The data toggle on the wire is not tracked: the controller model keeps +//! each endpoint's toggle and a lost transfer cannot be replayed here. +//! Remote wakeup, the wheel, and the host keyboard layout are not modelled. + +use crate::dev::ohci::{Pid, UsbFunction, UsbOutcome, UsbPacket}; +use crate::dev::ps2::HostInputSink; +use parking_lot::Mutex; +use std::collections::VecDeque; +use std::sync::Arc; +use winit::keyboard::KeyCode; + +/// A vendor and product pair from the pid.codes open test allocation, since +/// the devices are not products of any vendor. +const VENDOR_ID: u16 = 0x1209; +const PRODUCT_ID: u16 = 0x0001; + +// Standard request codes (USB 1.1 table 9-4) and descriptor types (table 9-5). +const GET_STATUS: u8 = 0; +const CLEAR_FEATURE: u8 = 1; +const SET_FEATURE: u8 = 3; +const SET_ADDRESS: u8 = 5; +const GET_DESCRIPTOR: u8 = 6; +const GET_CONFIGURATION: u8 = 8; +const SET_CONFIGURATION: u8 = 9; +const GET_INTERFACE: u8 = 10; +const SET_INTERFACE: u8 = 11; +const DESC_DEVICE: u8 = 1; +const DESC_CONFIGURATION: u8 = 2; +const DESC_STRING: u8 = 3; +const DESC_HID: u8 = 0x21; +const DESC_REPORT: u8 = 0x22; +// HID class requests (HID 1.11 section 7.2). +const GET_REPORT: u8 = 1; +const GET_IDLE: u8 = 2; +const GET_PROTOCOL: u8 = 3; +const SET_REPORT: u8 = 9; +const SET_IDLE: u8 = 10; +const SET_PROTOCOL: u8 = 11; + +/// HID 1.11 appendix B.1: the boot keyboard's report descriptor. Input: a +/// modifier bit per key and six key usages; output: five LED bits. +pub const KEYBOARD_REPORT_DESCRIPTOR: [u8; 63] = [ + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, 0x01, + 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01, + 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06, + 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xc0, +]; +/// HID 1.11 appendix B.2: the boot mouse's report descriptor. Three buttons +/// and relative X and Y. +pub const MOUSE_REPORT_DESCRIPTOR: [u8; 50] = [ + 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x03, + 0x15, 0x00, 0x25, 0x01, 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05, 0x81, 0x01, + 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7f, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, + 0xc0, 0xc0, +]; + +/// Reports held for the guest before the oldest is dropped. +const QUEUE_LIMIT: usize = 64; + +/// What differs between the two devices: the report they send and how host +/// input becomes one. +pub trait Boot: Send { + const PRODUCT: &'static str; + /// bInterfaceProtocol: 1 keyboard, 2 mouse. + const PROTOCOL: u8; + const REPORT_DESCRIPTOR: &'static [u8]; + const MAX_PACKET: u8; + /// The idle rate at reset, in 4 ms units (HID 1.11 section 7.2.4). + const DEFAULT_IDLE: u8; + fn reset(&mut self); + /// The report GET_REPORT returns now. + fn current(&self) -> Vec; + /// The next queued report, if any. + fn next_report(&mut self) -> Option>; + /// An output report; false when the device has none. + fn set_output(&mut self, _data: &[u8]) -> bool { + false + } + /// Host input waiting for the guest, in reports. + fn backlog(&self) -> usize; +} + +enum Stage { + Idle, + /// Data stage toward the host, then an OUT status. + In { data: Vec, sent: usize }, + /// Data stage from the host for a SET_REPORT of `length` bytes. + Out { length: usize, data: Vec }, + /// Status stage of a request with no data toward the host: the next IN + /// answers with an empty packet and the request takes effect. + StatusIn(Effect), +} + +#[derive(Clone, Copy)] +enum Effect { + None, + Address(u8), + Configuration(u8), + Idle(u8), + Protocol(u8), + Output, +} + +struct Inner { + dev: B, + address: u8, + configured: bool, + /// 0 boot, 1 report; a device starts in report protocol. + protocol: u8, + idle: u8, + remote_wakeup: bool, + stage: Stage, + last_report_ms: u64, +} + +pub struct UsbHid { + inner: Mutex>, +} + +impl UsbHid { + fn with(dev: B) -> Arc { + Arc::new(Self { + inner: Mutex::new(Inner { + dev, + address: 0, + configured: false, + protocol: 1, + idle: B::DEFAULT_IDLE, + remote_wakeup: false, + stage: Stage::Idle, + last_report_ms: 0, + }), + }) + } +} + +fn string_descriptor(s: &str) -> Vec { + let units: Vec = s.encode_utf16().flat_map(u16::to_le_bytes).collect(); + let mut d = vec![2 + units.len() as u8, DESC_STRING]; + d.extend(units); + d +} + +impl Inner { + fn device_descriptor() -> Vec { + let mut d = vec![18, DESC_DEVICE, 0x10, 0x01, 0, 0, 0, 8]; + d.extend(VENDOR_ID.to_le_bytes()); + d.extend(PRODUCT_ID.to_le_bytes()); + // bcdDevice, manufacturer and product strings, no serial, one configuration. + d.extend([0x00, 0x01, 1, 2, 0, 1]); + d + } + + fn hid_descriptor() -> Vec { + let n = B::REPORT_DESCRIPTOR.len() as u16; + let mut d = vec![9, DESC_HID, 0x11, 0x01, 0, 1, DESC_REPORT]; + d.extend(n.to_le_bytes()); + d + } + + /// The configuration descriptor followed by its interface, HID and + /// endpoint descriptors, as GET_DESCRIPTOR(configuration) returns them. + fn configuration_descriptor() -> Vec { + let interface = [9, 4, 0, 0, 1, 3, 1, B::PROTOCOL, 0]; + let hid = Self::hid_descriptor(); + // Interrupt IN endpoint 1, polled every 10 ms. + let endpoint = [7, 5, 0x81, 3, B::MAX_PACKET, 0, 10]; + let total = (9 + interface.len() + hid.len() + endpoint.len()) as u16; + let mut d = vec![9, DESC_CONFIGURATION]; + d.extend(total.to_le_bytes()); + // One interface, configuration 1, no string, bus powered with remote + // wakeup, 100 mA. + d.extend([1, 1, 0, 0xa0, 50]); + d.extend(interface); + d.extend(hid); + d.extend(endpoint); + d + } + + fn string(index: u8) -> Option> { + match index { + 0 => Some(vec![4, DESC_STRING, 0x09, 0x04]), + 1 => Some(string_descriptor("iris")), + 2 => Some(string_descriptor(B::PRODUCT)), + _ => None, + } + } + + fn reset(&mut self) { + self.address = 0; + self.configured = false; + self.protocol = 1; + self.idle = B::DEFAULT_IDLE; + self.remote_wakeup = false; + self.stage = Stage::Idle; + self.dev.reset(); + } + + /// Decode a SETUP packet (USB 1.1 table 9-2). None is a request this + /// device does not have, answered with a STALL. + fn setup(&mut self, p: &[u8]) -> Option<()> { + let [rt, req, vl, vh, il, _ih, ll, lh] = <[u8; 8]>::try_from(p).ok()?; + let value = u16::from_le_bytes([vl, vh]); + let length = u16::from_le_bytes([ll, lh]) as usize; + let reply = |data: Vec| Stage::In { sent: 0, data: data[..data.len().min(length)].to_vec() }; + let (kind, recipient) = ((rt >> 5) & 3, rt & 0x1f); + let to_host = rt & 0x80 != 0; + self.stage = match (kind, recipient, to_host, req) { + // Standard requests (USB 1.1 section 9.4). + (0, 0, true, GET_STATUS) => reply(vec![u8::from(self.remote_wakeup) << 1, 0]), + (0, 1 | 2, true, GET_STATUS) if value == 0 => reply(vec![0, 0]), + (0, 0, false, CLEAR_FEATURE) if value == 1 => { + self.remote_wakeup = false; + Stage::StatusIn(Effect::None) + } + (0, 0, false, SET_FEATURE) if value == 1 => { + self.remote_wakeup = true; + Stage::StatusIn(Effect::None) + } + // Clearing a halt on the interrupt endpoint is accepted; nothing halts it. + (0, 2, false, CLEAR_FEATURE) if value == 0 && il & 0x8f == 0x81 => Stage::StatusIn(Effect::None), + (0, 0, false, SET_ADDRESS) if value <= 127 => Stage::StatusIn(Effect::Address(value as u8)), + (0, 0, true, GET_DESCRIPTOR) => match (vh, vl) { + (DESC_DEVICE, 0) => reply(Self::device_descriptor()), + (DESC_CONFIGURATION, 0) => reply(Self::configuration_descriptor()), + (DESC_STRING, n) => reply(Self::string(n)?), + _ => return None, + }, + (0, 0, true, GET_CONFIGURATION) => reply(vec![u8::from(self.configured)]), + (0, 0, false, SET_CONFIGURATION) if value <= 1 => Stage::StatusIn(Effect::Configuration(value as u8)), + (0, 1, true, GET_INTERFACE) if il == 0 => reply(vec![0]), + (0, 1, false, SET_INTERFACE) if il == 0 && value == 0 => Stage::StatusIn(Effect::None), + // The HID descriptor and report descriptor belong to the interface. + (0, 1, true, GET_DESCRIPTOR) if il == 0 => match (vh, vl) { + (DESC_HID, 0) => reply(Self::hid_descriptor()), + (DESC_REPORT, 0) => reply(B::REPORT_DESCRIPTOR.to_vec()), + _ => return None, + }, + // HID class requests (HID 1.11 section 7.2), interface 0. + (1, 1, true, GET_REPORT) if il == 0 && vh == 1 && vl == 0 => reply(self.dev.current()), + (1, 1, false, SET_REPORT) if il == 0 && vh == 2 && vl == 0 && length > 0 => { + Stage::Out { length, data: Vec::new() } + } + (1, 1, true, GET_IDLE) if il == 0 => reply(vec![self.idle]), + (1, 1, false, SET_IDLE) if il == 0 => Stage::StatusIn(Effect::Idle(vh)), + (1, 1, true, GET_PROTOCOL) if il == 0 => reply(vec![self.protocol]), + (1, 1, false, SET_PROTOCOL) if il == 0 && value <= 1 => Stage::StatusIn(Effect::Protocol(value as u8)), + _ => return None, + }; + Some(()) + } + + fn apply(&mut self, effect: Effect, data: &[u8], now_ms: u64) -> bool { + match effect { + Effect::None => {} + Effect::Address(a) => self.address = a, + Effect::Configuration(c) => { + self.configured = c != 0; + self.dev.reset(); + self.last_report_ms = now_ms; + } + Effect::Idle(rate) => self.idle = rate, + Effect::Protocol(p) => self.protocol = p, + Effect::Output => return self.dev.set_output(data), + } + true + } + + fn control(&mut self, p: UsbPacket) -> UsbOutcome { + match p.pid { + Pid::Setup => match self.setup(&p.data) { + Some(()) => UsbOutcome::Ack(Vec::new()), + None => { + self.stage = Stage::Idle; + UsbOutcome::Stall + } + }, + Pid::In => match &mut self.stage { + Stage::In { data, sent } => { + let n = p.capacity.min(data.len() - *sent); + let chunk = data[*sent..*sent + n].to_vec(); + *sent += n; + UsbOutcome::Ack(chunk) + } + Stage::StatusIn(effect) => { + let effect = *effect; + self.stage = Stage::Idle; + if self.apply(effect, &[], p.time_ms) { UsbOutcome::Ack(Vec::new()) } else { UsbOutcome::Stall } + } + _ => UsbOutcome::Stall, + }, + Pid::Out => match &mut self.stage { + // The status stage of a request that sent data to the host. + Stage::In { .. } if p.data.is_empty() => { + self.stage = Stage::Idle; + UsbOutcome::Ack(Vec::new()) + } + Stage::Out { length, data } => { + data.extend(&p.data); + if data.len() >= *length { + let data = std::mem::take(data); + let ok = self.apply(Effect::Output, &data, p.time_ms); + self.stage = if ok { Stage::StatusIn(Effect::None) } else { Stage::Idle }; + if !ok { + return UsbOutcome::Stall; + } + } + UsbOutcome::Ack(Vec::new()) + } + _ => UsbOutcome::Stall, + }, + } + } + + /// Interrupt IN endpoint 1 (USB 1.1 section 5.7): a report when there is + /// one, otherwise NAK. A nonzero idle rate repeats the last state once the + /// period passes without a change. + fn interrupt(&mut self, p: UsbPacket) -> UsbOutcome { + if !self.configured { + return UsbOutcome::Stall; + } + if let Some(r) = self.dev.next_report() { + self.last_report_ms = p.time_ms; + return UsbOutcome::Ack(r); + } + if self.idle != 0 && p.time_ms.saturating_sub(self.last_report_ms) >= u64::from(self.idle) * 4 { + self.last_report_ms = p.time_ms; + return UsbOutcome::Ack(self.dev.current()); + } + UsbOutcome::Nak + } +} + +impl UsbFunction for UsbHid { + fn reset(&self) { + self.inner.lock().reset(); + } + fn address(&self) -> u8 { + self.inner.lock().address + } + fn low_speed(&self) -> bool { + true + } + fn transact(&self, packet: UsbPacket) -> UsbOutcome { + let mut s = self.inner.lock(); + match packet.endpoint { + 0 => s.control(packet), + 1 if packet.pid == Pid::In => s.interrupt(packet), + _ => UsbOutcome::Stall, + } + } +} + +// --------------------------------------------------------------------------- +// Keyboard +// --------------------------------------------------------------------------- + +/// The HID usage (keyboard/keypad page 0x07) of a host key, or None for a +/// key the page has no entry for. +pub fn usage(key: KeyCode) -> Option { + use KeyCode::*; + Some(match key { + KeyA => 0x04, KeyB => 0x05, KeyC => 0x06, KeyD => 0x07, KeyE => 0x08, KeyF => 0x09, + KeyG => 0x0a, KeyH => 0x0b, KeyI => 0x0c, KeyJ => 0x0d, KeyK => 0x0e, KeyL => 0x0f, + KeyM => 0x10, KeyN => 0x11, KeyO => 0x12, KeyP => 0x13, KeyQ => 0x14, KeyR => 0x15, + KeyS => 0x16, KeyT => 0x17, KeyU => 0x18, KeyV => 0x19, KeyW => 0x1a, KeyX => 0x1b, + KeyY => 0x1c, KeyZ => 0x1d, + Digit1 => 0x1e, Digit2 => 0x1f, Digit3 => 0x20, Digit4 => 0x21, Digit5 => 0x22, + Digit6 => 0x23, Digit7 => 0x24, Digit8 => 0x25, Digit9 => 0x26, Digit0 => 0x27, + Enter => 0x28, Escape => 0x29, Backspace => 0x2a, Tab => 0x2b, Space => 0x2c, + Minus => 0x2d, Equal => 0x2e, BracketLeft => 0x2f, BracketRight => 0x30, + Backslash => 0x31, Semicolon => 0x33, Quote => 0x34, Backquote => 0x35, + Comma => 0x36, Period => 0x37, Slash => 0x38, CapsLock => 0x39, + F1 => 0x3a, F2 => 0x3b, F3 => 0x3c, F4 => 0x3d, F5 => 0x3e, F6 => 0x3f, + F7 => 0x40, F8 => 0x41, F9 => 0x42, F10 => 0x43, F11 => 0x44, F12 => 0x45, + PrintScreen => 0x46, ScrollLock => 0x47, Pause => 0x48, Insert => 0x49, + Home => 0x4a, PageUp => 0x4b, Delete => 0x4c, End => 0x4d, PageDown => 0x4e, + ArrowRight => 0x4f, ArrowLeft => 0x50, ArrowDown => 0x51, ArrowUp => 0x52, + NumLock => 0x53, NumpadDivide => 0x54, NumpadMultiply => 0x55, + NumpadSubtract => 0x56, NumpadAdd => 0x57, NumpadEnter => 0x58, + Numpad1 => 0x59, Numpad2 => 0x5a, Numpad3 => 0x5b, Numpad4 => 0x5c, + Numpad5 => 0x5d, Numpad6 => 0x5e, Numpad7 => 0x5f, Numpad8 => 0x60, + Numpad9 => 0x61, Numpad0 => 0x62, NumpadDecimal => 0x63, + IntlBackslash => 0x64, ContextMenu => 0x65, + _ => return None, + }) +} + +/// The bit of a modifier key in the report's first byte (usages 0xe0 to 0xe7). +fn modifier(key: KeyCode) -> Option { + use KeyCode::*; + Some(match key { + ControlLeft => 0x01, ShiftLeft => 0x02, AltLeft => 0x04, SuperLeft => 0x08, + ControlRight => 0x10, ShiftRight => 0x20, AltRight => 0x40, SuperRight => 0x80, + _ => return None, + }) +} + +#[derive(Default)] +pub struct Keyboard { + modifiers: u8, + /// Pressed keys in the order they went down. + keys: Vec, + queue: VecDeque>, + leds: u8, +} + +impl Keyboard { + /// The 8-byte boot report (HID 1.11 B.1): modifiers, reserved, six keys. + /// More than six keys at once is the phantom state, ErrorRollOver in every slot. + fn report(&self) -> Vec { + let mut r = vec![self.modifiers, 0, 0, 0, 0, 0, 0, 0]; + if self.keys.len() > 6 { + r[2..].fill(0x01); + } else { + r[2..2 + self.keys.len()].copy_from_slice(&self.keys); + } + r + } + + fn push(&mut self) { + if self.queue.len() >= QUEUE_LIMIT { + self.queue.pop_front(); + } + let report = self.report(); + self.queue.push_back(report); + } +} + +impl Boot for Keyboard { + const PRODUCT: &'static str = "USB Keyboard"; + const PROTOCOL: u8 = 1; + const REPORT_DESCRIPTOR: &'static [u8] = &KEYBOARD_REPORT_DESCRIPTOR; + const MAX_PACKET: u8 = 8; + // 500 ms, the recommended keyboard default. + const DEFAULT_IDLE: u8 = 125; + fn reset(&mut self) { + *self = Keyboard::default(); + } + fn current(&self) -> Vec { + self.report() + } + fn next_report(&mut self) -> Option> { + self.queue.pop_front() + } + /// The LED output report (HID 1.11 B.1): num lock, caps lock, scroll + /// lock, compose, kana in bits 0 to 4. + fn set_output(&mut self, data: &[u8]) -> bool { + self.leds = data.first().copied().unwrap_or(0) & 0x1f; + true + } + fn backlog(&self) -> usize { + self.queue.len() + } +} + +pub type UsbKeyboard = UsbHid; + +impl UsbKeyboard { + pub fn new() -> Arc { + Self::with(Keyboard::default()) + } + + /// The LEDs the guest last asked for. + pub fn leds(&self) -> u8 { + self.inner.lock().dev.leds + } +} + +impl HostInputSink for UsbKeyboard { + fn key(&self, key: KeyCode, pressed: bool) { + let mut s = self.inner.lock(); + if !s.configured { + return; + } + let k = &mut s.dev; + if let Some(bit) = modifier(key) { + let before = k.modifiers; + if pressed { k.modifiers |= bit } else { k.modifiers &= !bit } + if k.modifiers != before { + k.push(); + } + } else if let Some(u) = usage(key) { + let at = k.keys.iter().position(|&x| x == u); + match (pressed, at) { + (true, None) => k.keys.push(u), + (false, Some(i)) => { + k.keys.remove(i); + } + _ => return, + } + k.push(); + } + } + fn mouse(&self, _: u8, _: i32, _: i32, _: i32) {} + fn mouse_enabled(&self) -> bool { + false + } + fn mouse_pending(&self) -> usize { + 0 + } +} + +// --------------------------------------------------------------------------- +// Mouse +// --------------------------------------------------------------------------- + +#[derive(Default)] +pub struct Mouse { + buttons: u8, + /// Movement not yet in a report. + dx: i32, + dy: i32, + queue: VecDeque>, +} + +impl Mouse { + /// Move accumulated motion into reports, at most a signed byte on each + /// axis apiece (HID 1.11 B.2: buttons, X, Y). `force` sends one even + /// with no motion, for a button change. + fn flush(&mut self, mut force: bool) { + while (force || self.dx != 0 || self.dy != 0) && self.queue.len() < QUEUE_LIMIT { + let (x, y) = (self.dx.clamp(-127, 127), self.dy.clamp(-127, 127)); + self.dx -= x; + self.dy -= y; + self.queue.push_back(vec![self.buttons, x as i8 as u8, y as i8 as u8]); + force = false; + } + } +} + +impl Boot for Mouse { + const PRODUCT: &'static str = "USB Mouse"; + const PROTOCOL: u8 = 2; + const REPORT_DESCRIPTOR: &'static [u8] = &MOUSE_REPORT_DESCRIPTOR; + const MAX_PACKET: u8 = 4; + // A mouse reports only on change. + const DEFAULT_IDLE: u8 = 0; + fn reset(&mut self) { + *self = Mouse::default(); + } + fn current(&self) -> Vec { + vec![self.buttons, 0, 0] + } + fn next_report(&mut self) -> Option> { + if self.queue.is_empty() { + self.flush(false); + } + self.queue.pop_front() + } + fn backlog(&self) -> usize { + self.queue.len() + usize::from(self.dx != 0 || self.dy != 0) + } +} + +pub type UsbMouse = UsbHid; + +impl UsbMouse { + pub fn new() -> Arc { + Self::with(Mouse::default()) + } +} + +impl HostInputSink for UsbMouse { + fn key(&self, _: KeyCode, _: bool) {} + fn mouse(&self, buttons: u8, dx: i32, dy: i32, _dz: i32) { + let mut s = self.inner.lock(); + if !s.configured { + return; + } + let m = &mut s.dev; + m.dx = m.dx.saturating_add(dx); + m.dy = m.dy.saturating_add(dy); + let buttons = buttons & 7; + // Motion up to now travels with the old button state. + if buttons != m.buttons { + m.flush(false); + m.buttons = buttons; + m.flush(true); + } else { + m.flush(false); + } + } + fn mouse_enabled(&self) -> bool { + self.inner.lock().configured + } + fn mouse_pending(&self) -> usize { + self.inner.lock().dev.backlog() + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/usb_hid/tests.rs b/src/dev/usb_hid/tests.rs new file mode 100644 index 000000000..5efe1a8b2 --- /dev/null +++ b/src/dev/usb_hid/tests.rs @@ -0,0 +1,448 @@ +// The cases follow USB 1.1 chapter 9 and HID 1.11: descriptor layout and +// lengths, the control pipe's stages, and the boot report formats. The last +// ones put a device behind the OHCI model to show an interrupt transfer +// returns a report through guest memory. +use super::*; +use crate::dev::ohci::Ohci; +use crate::pci::{PciBusDevice, PciDma, PciDmaStatus}; +use crate::traits::Device; + +fn packet(endpoint: u8, pid: Pid, capacity: usize, data: &[u8], time_ms: u64) -> UsbPacket { + UsbPacket { endpoint, pid, toggle: false, capacity, data: data.to_vec(), time_ms } +} + +fn ack(o: UsbOutcome) -> Vec { + match o { + UsbOutcome::Ack(b) => b, + other => panic!("expected an ACK, got {other:?}"), + } +} + +/// A control transfer toward the host: SETUP, IN packets of 8 until short, +/// then the OUT status. +fn control_in(f: &dyn UsbFunction, rt: u8, req: u8, value: u16, index: u16, length: u16) -> Result, UsbOutcome> { + let mut setup = vec![rt, req]; + setup.extend(value.to_le_bytes()); + setup.extend(index.to_le_bytes()); + setup.extend(length.to_le_bytes()); + match f.transact(packet(0, Pid::Setup, 8, &setup, 0)) { + UsbOutcome::Ack(_) => {} + other => return Err(other), + } + let mut data = Vec::new(); + loop { + let chunk = ack(f.transact(packet(0, Pid::In, 8, &[], 0))); + let short = chunk.len() < 8; + data.extend(chunk); + if short || data.len() >= length as usize { + break; + } + } + ack(f.transact(packet(0, Pid::Out, 8, &[], 0))); + Ok(data) +} + +/// A control transfer with no data stage: SETUP then the IN status. +fn control_nodata(f: &dyn UsbFunction, rt: u8, req: u8, value: u16, index: u16) -> UsbOutcome { + let mut setup = vec![rt, req]; + setup.extend(value.to_le_bytes()); + setup.extend(index.to_le_bytes()); + setup.extend(0u16.to_le_bytes()); + match f.transact(packet(0, Pid::Setup, 8, &setup, 0)) { + UsbOutcome::Ack(_) => f.transact(packet(0, Pid::In, 8, &[], 0)), + other => other, + } +} + +fn enumerate(f: &dyn UsbFunction, address: u8) { + f.reset(); + assert!(ack(control_nodata(f, 0x00, SET_ADDRESS, address as u16, 0)).is_empty()); + assert_eq!(f.address(), address); + assert!(ack(control_nodata(f, 0x00, SET_CONFIGURATION, 1, 0)).is_empty()); +} + +fn interrupt(f: &dyn UsbFunction, time_ms: u64) -> UsbOutcome { + f.transact(packet(1, Pid::In, 8, &[], time_ms)) +} + +#[test] +fn the_device_descriptor_names_a_low_speed_hid_function() { + for f in [&*UsbKeyboard::new() as &dyn UsbFunction, &*UsbMouse::new()] { + assert!(f.low_speed()); + let d = control_in(f, 0x80, GET_DESCRIPTOR, 0x0100, 0, 64).unwrap(); + assert_eq!(d.len(), 18); + assert_eq!(d[0], 18, "bLength"); + assert_eq!(d[1], 1, "bDescriptorType"); + assert_eq!(u16::from_le_bytes([d[2], d[3]]), 0x0110, "USB 1.1"); + assert_eq!(d[4], 0, "the class is in the interface"); + assert_eq!(d[7], 8, "a low-speed control pipe carries 8 bytes"); + assert_eq!(d[17], 1, "one configuration"); + // The guest may read just the first 8 bytes before addressing. + assert_eq!(control_in(f, 0x80, GET_DESCRIPTOR, 0x0100, 0, 8).unwrap(), d[..8]); + } +} + +/// A configuration, its interface, the HID descriptor and the endpoint come +/// back in one block whose wTotalLength covers them (USB 1.1 9.6.2). +#[test] +fn the_configuration_block_describes_one_boot_interface_and_one_interrupt_endpoint() { + for (f, protocol, report_len, packet_size) in [ + (&*UsbKeyboard::new() as &dyn UsbFunction, 1u8, KEYBOARD_REPORT_DESCRIPTOR.len(), 8u8), + (&*UsbMouse::new(), 2, MOUSE_REPORT_DESCRIPTOR.len(), 4), + ] { + let c = control_in(f, 0x80, GET_DESCRIPTOR, 0x0200, 0, 255).unwrap(); + assert_eq!(c.len(), 34); + assert_eq!(&c[..2], [9, 2]); + assert_eq!(u16::from_le_bytes([c[2], c[3]]) as usize, c.len(), "wTotalLength"); + assert_eq!((c[4], c[5]), (1, 1), "one interface; configuration value 1"); + assert_eq!(&c[9..14], [9, 4, 0, 0, 1], "interface 0, no alternate, one endpoint"); + assert_eq!(&c[14..17], [3, 1, protocol], "HID class, boot subclass, keyboard or mouse"); + assert_eq!(&c[18..20], [9, 0x21], "HID descriptor"); + assert_eq!(u16::from_le_bytes([c[20], c[21]]), 0x0111, "bcdHID 1.11"); + assert_eq!(&c[24..26], [0x22, report_len as u8], "one report descriptor of its length"); + assert_eq!(&c[27..], [7, 5, 0x81, 3, packet_size, 0, 10], "interrupt IN 1, polled at 10 ms"); + // The HID descriptor is also readable on its own through the interface. + assert_eq!(control_in(f, 0x81, GET_DESCRIPTOR, 0x2100, 0, 9).unwrap(), c[18..27]); + } +} + +#[test] +fn the_report_descriptors_are_the_boot_descriptors() { + let k = UsbKeyboard::new(); + let r = control_in(&*k, 0x81, GET_DESCRIPTOR, 0x2200, 0, 255).unwrap(); + assert_eq!(r, KEYBOARD_REPORT_DESCRIPTOR); + // Usage page generic desktop, usage keyboard, application collection. + assert_eq!(&r[..6], [0x05, 0x01, 0x09, 0x06, 0xa1, 0x01]); + assert_eq!(r.last(), Some(&0xc0), "collection ends"); + let m = UsbMouse::new(); + let r = control_in(&*m, 0x81, GET_DESCRIPTOR, 0x2200, 0, 255).unwrap(); + assert_eq!(r, MOUSE_REPORT_DESCRIPTOR); + assert_eq!(&r[..6], [0x05, 0x01, 0x09, 0x02, 0xa1, 0x01]); +} + +#[test] +fn strings_are_utf16_with_a_language_table() { + let k = UsbKeyboard::new(); + assert_eq!(control_in(&*k, 0x80, GET_DESCRIPTOR, 0x0300, 0, 255).unwrap(), [4, 3, 0x09, 0x04]); + let s = control_in(&*k, 0x80, GET_DESCRIPTOR, 0x0302, 0x0409, 255).unwrap(); + assert_eq!(s[0] as usize, s.len()); + let text: Vec = s[2..].chunks(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect(); + assert_eq!(String::from_utf16(&text).unwrap(), "USB Keyboard"); + assert!(control_in(&*k, 0x80, GET_DESCRIPTOR, 0x0309, 0x0409, 255).is_err(), "no such string"); +} + +/// SET_ADDRESS takes effect after its status stage, not before (USB 1.1 9.4.6). +#[test] +fn set_address_applies_after_the_status_stage() { + let m = UsbMouse::new(); + let mut setup = vec![0x00, SET_ADDRESS, 9, 0, 0, 0, 0, 0]; + assert!(ack(m.transact(packet(0, Pid::Setup, 8, &setup, 0))).is_empty()); + assert_eq!(m.address(), 0, "still the default address during the status stage"); + assert!(ack(m.transact(packet(0, Pid::In, 8, &[], 0))).is_empty()); + assert_eq!(m.address(), 9); + // An address above 127 is not one. + setup[2] = 200; + assert_eq!(m.transact(packet(0, Pid::Setup, 8, &setup, 0)), UsbOutcome::Stall); + // A bus reset returns the device to the default address. + m.reset(); + assert_eq!(m.address(), 0); +} + +#[test] +fn configuration_state_is_readable_and_unknown_requests_stall() { + let k = UsbKeyboard::new(); + assert_eq!(control_in(&*k, 0x80, GET_CONFIGURATION, 0, 0, 1).unwrap(), [0]); + assert!(ack(control_nodata(&*k, 0x00, SET_CONFIGURATION, 1, 0)).is_empty()); + assert_eq!(control_in(&*k, 0x80, GET_CONFIGURATION, 0, 0, 1).unwrap(), [1]); + assert_eq!(control_nodata(&*k, 0x00, SET_CONFIGURATION, 2, 0), UsbOutcome::Stall); + assert_eq!(control_in(&*k, 0x81, GET_INTERFACE, 0, 0, 1).unwrap(), [0]); + assert_eq!(control_in(&*k, 0x80, GET_STATUS, 0, 0, 2).unwrap(), [0, 0]); + // No device qualifier on a low-speed-only device; no such request. + assert!(control_in(&*k, 0x80, GET_DESCRIPTOR, 0x0600, 0, 10).is_err()); + assert_eq!(control_nodata(&*k, 0x00, 0x77, 0, 0), UsbOutcome::Stall); + // A stalled request leaves the pipe usable for the next SETUP. + assert_eq!(control_in(&*k, 0x80, GET_CONFIGURATION, 0, 0, 1).unwrap(), [1]); + // The interrupt endpoint is not there before a configuration is selected. + let fresh = UsbKeyboard::new(); + assert_eq!(interrupt(&*fresh, 0), UsbOutcome::Stall); +} + +#[test] +fn hid_class_requests_set_and_report_idle_and_protocol() { + let k = UsbKeyboard::new(); + // A device starts in report protocol with the keyboard's 500 ms idle. + assert_eq!(control_in(&*k, 0xa1, GET_PROTOCOL, 0, 0, 1).unwrap(), [1]); + assert_eq!(control_in(&*k, 0xa1, GET_IDLE, 0, 0, 1).unwrap(), [125]); + assert!(ack(control_nodata(&*k, 0x21, SET_PROTOCOL, 0, 0)).is_empty()); + assert_eq!(control_in(&*k, 0xa1, GET_PROTOCOL, 0, 0, 1).unwrap(), [0]); + // SET_IDLE carries the duration in the high byte of wValue. + assert!(ack(control_nodata(&*k, 0x21, SET_IDLE, 0, 0)).is_empty()); + assert_eq!(control_in(&*k, 0xa1, GET_IDLE, 0, 0, 1).unwrap(), [0]); + assert_eq!(control_nodata(&*k, 0x21, SET_PROTOCOL, 2, 0), UsbOutcome::Stall); + // A mouse starts with no idle repeat. + let m = UsbMouse::new(); + assert_eq!(control_in(&*m, 0xa1, GET_IDLE, 0, 0, 1).unwrap(), [0]); +} + +#[test] +fn set_report_takes_the_keyboards_leds_and_a_mouse_has_none() { + let k = UsbKeyboard::new(); + let setup = [0x21, SET_REPORT, 0, 2, 0, 0, 1, 0]; + assert!(ack(k.transact(packet(0, Pid::Setup, 8, &setup, 0))).is_empty()); + assert!(ack(k.transact(packet(0, Pid::Out, 8, &[0x1f | 0x80], 0))).is_empty()); + assert!(ack(k.transact(packet(0, Pid::In, 8, &[], 0))).is_empty(), "status stage"); + assert_eq!(k.leds(), 0x1f, "five LED bits"); + let m = UsbMouse::new(); + assert!(ack(m.transact(packet(0, Pid::Setup, 8, &setup, 0))).is_empty()); + assert_eq!(m.transact(packet(0, Pid::Out, 8, &[1], 0)), UsbOutcome::Stall); +} + +#[test] +fn keyboard_reports_carry_modifiers_and_keys_in_press_order() { + let k = UsbKeyboard::new(); + enumerate(&*k, 3); + assert_eq!(interrupt(&*k, 1), UsbOutcome::Nak, "nothing to report yet"); + k.key(KeyCode::ShiftLeft, true); + k.key(KeyCode::KeyA, true); + k.key(KeyCode::KeyB, true); + k.key(KeyCode::KeyA, false); + k.key(KeyCode::ShiftLeft, false); + k.key(KeyCode::KeyB, false); + let reports: Vec> = (0..6).map(|i| ack(interrupt(&*k, i))).collect(); + assert_eq!(reports, [ + vec![0x02, 0, 0, 0, 0, 0, 0, 0], + vec![0x02, 0, 0x04, 0, 0, 0, 0, 0], + vec![0x02, 0, 0x04, 0x05, 0, 0, 0, 0], + vec![0x02, 0, 0x05, 0, 0, 0, 0, 0], + vec![0x00, 0, 0x05, 0, 0, 0, 0, 0], + vec![0x00, 0, 0, 0, 0, 0, 0, 0], + ], "a tap between polls still reports both edges"); + assert_eq!(interrupt(&*k, 7), UsbOutcome::Nak); +} + +#[test] +fn keyboard_usages_follow_the_usage_table() { + assert_eq!(usage(KeyCode::KeyA), Some(0x04)); + assert_eq!(usage(KeyCode::KeyZ), Some(0x1d)); + assert_eq!(usage(KeyCode::Digit1), Some(0x1e)); + assert_eq!(usage(KeyCode::Digit0), Some(0x27)); + assert_eq!(usage(KeyCode::Enter), Some(0x28)); + assert_eq!(usage(KeyCode::Escape), Some(0x29)); + assert_eq!(usage(KeyCode::Space), Some(0x2c)); + assert_eq!(usage(KeyCode::F1), Some(0x3a)); + assert_eq!(usage(KeyCode::F12), Some(0x45)); + assert_eq!(usage(KeyCode::ArrowRight), Some(0x4f)); + assert_eq!(usage(KeyCode::ArrowUp), Some(0x52)); + assert_eq!(usage(KeyCode::Numpad0), Some(0x62)); + assert_eq!(usage(KeyCode::ControlLeft), None, "modifiers have a report bit, not a usage slot"); + assert_eq!(modifier(KeyCode::ControlLeft), Some(0x01)); + assert_eq!(modifier(KeyCode::SuperRight), Some(0x80)); +} + +#[test] +fn a_seventh_key_reports_rollover_and_release_recovers() { + let k = UsbKeyboard::new(); + enumerate(&*k, 1); + let keys = [KeyCode::KeyA, KeyCode::KeyB, KeyCode::KeyC, KeyCode::KeyD, KeyCode::KeyE, KeyCode::KeyF, KeyCode::KeyG]; + for key in keys { + k.key(key, true); + } + let reports: Vec> = (0..7).map(|i| ack(interrupt(&*k, i))).collect(); + assert_eq!(reports[5], [0, 0, 4, 5, 6, 7, 8, 9]); + assert_eq!(reports[6], [0, 0, 1, 1, 1, 1, 1, 1], "ErrorRollOver"); + k.key(KeyCode::KeyG, false); + assert_eq!(ack(interrupt(&*k, 8)), [0, 0, 4, 5, 6, 7, 8, 9]); + // A repeated press of a held key is not a new edge. + k.key(KeyCode::KeyA, true); + assert_eq!(interrupt(&*k, 9), UsbOutcome::Nak); +} + +#[test] +fn input_before_configuration_is_dropped_and_get_report_reads_the_state() { + let k = UsbKeyboard::new(); + k.key(KeyCode::KeyA, true); + enumerate(&*k, 2); + assert_eq!(interrupt(&*k, 1), UsbOutcome::Nak, "the early key never reached the report queue"); + k.key(KeyCode::KeyQ, true); + assert_eq!(control_in(&*k, 0xa1, GET_REPORT, 0x0100, 0, 8).unwrap(), [0, 0, 0x14, 0, 0, 0, 0, 0]); +} + +/// With a nonzero idle rate the keyboard repeats its state when the period +/// passes without a change (HID 1.11 7.2.4), and with zero it stays quiet. +#[test] +fn the_idle_rate_repeats_the_last_report() { + let k = UsbKeyboard::new(); + enumerate(&*k, 2); + k.key(KeyCode::KeyA, true); + assert_eq!(ack(interrupt(&*k, 10)), [0, 0, 4, 0, 0, 0, 0, 0]); + assert_eq!(interrupt(&*k, 509), UsbOutcome::Nak, "500 ms not yet passed"); + assert_eq!(ack(interrupt(&*k, 510)), [0, 0, 4, 0, 0, 0, 0, 0]); + assert!(ack(control_nodata(&*k, 0x21, SET_IDLE, 0, 0)).is_empty()); + assert_eq!(interrupt(&*k, 5000), UsbOutcome::Nak); +} + +#[test] +fn mouse_reports_carry_buttons_and_signed_motion() { + let m = UsbMouse::new(); + enumerate(&*m, 4); + assert!(m.mouse_enabled()); + assert_eq!(interrupt(&*m, 1), UsbOutcome::Nak); + // Host dy is positive down the screen, as HID Y is. + m.mouse(0, 10, -20, 0); + assert_eq!(ack(interrupt(&*m, 2)), [0, 10, (-20i8) as u8]); + // A press travels with its own report, then the release. + m.mouse(1, 0, 0, 0); + m.mouse(0, 0, 0, 0); + assert_eq!(ack(interrupt(&*m, 3)), [1, 0, 0]); + assert_eq!(ack(interrupt(&*m, 4)), [0, 0, 0]); + // Buttons in PS/2 order: right is 2, middle is 4. + m.mouse(2 | 4, 0, 0, 0); + assert_eq!(ack(interrupt(&*m, 5)), [6, 0, 0]); + assert_eq!(control_in(&*m, 0xa1, GET_REPORT, 0x0100, 0, 3).unwrap(), [6, 0, 0]); + // Boot protocol has no wheel byte: the report stays 3 bytes. + m.mouse(6, 0, 0, 3); + assert_eq!(interrupt(&*m, 6), UsbOutcome::Nak); +} + +#[test] +fn large_motion_splits_across_reports_without_loss() { + let m = UsbMouse::new(); + enumerate(&*m, 4); + m.mouse(0, 300, -200, 0); + assert_eq!(m.mouse_pending(), 3); + let mut sum = (0i32, 0i32); + let mut n = 0; + while let UsbOutcome::Ack(r) = interrupt(&*m, n) { + sum.0 += r[1] as i8 as i32; + sum.1 += r[2] as i8 as i32; + assert!(r[1] as i8 != -128 && r[2] as i8 != -128); + n += 1; + } + assert_eq!(sum, (300, -200)); + assert_eq!(m.mouse_pending(), 0); +} + +#[test] +fn a_mouse_before_configuration_is_not_enabled_and_drops_motion() { + let m = UsbMouse::new(); + assert!(!m.mouse_enabled()); + m.mouse(1, 5, 5, 0); + assert_eq!(m.mouse_pending(), 0); + enumerate(&*m, 1); + assert_eq!(interrupt(&*m, 1), UsbOutcome::Nak); +} + +/// Host input arrives through the PS/2 controller's sinks, so the one +/// interface the GUI, VNC and CI use feeds both kinds of device. +#[test] +fn host_input_reaches_usb_devices_through_the_ps2_controller() { + use crate::dev::ps2::Ps2Controller; + let ps2 = Ps2Controller::new(None); + Device::start(&ps2); + let (k, m) = (UsbKeyboard::new(), UsbMouse::new()); + ps2.add_sink(k.clone()); + ps2.add_sink(m.clone()); + enumerate(&*k, 1); + enumerate(&*m, 2); + assert!(ps2.mouse_enabled(), "the USB mouse counts: the PS/2 mouse is still off"); + ps2.push_kb(KeyCode::KeyA, true); + ps2.type_char('b'); + ps2.push_mouse_input(1, 3, 4, 0); + assert_eq!(ps2.mouse_pending(), 2, "a move and a button change wait for the guest"); + assert_eq!(ack(interrupt(&*k, 1)), [0, 0, 4, 0, 0, 0, 0, 0]); + assert_eq!(ack(interrupt(&*k, 2)), [0, 0, 4, 5, 0, 0, 0, 0]); + assert_eq!(ack(interrupt(&*m, 1)), [0, 3, 4]); + assert_eq!(ack(interrupt(&*m, 2)), [1, 0, 0]); +} + +// ---- Through the OHCI model ------------------------------------------------ + +struct Memory(Mutex>); +impl Memory { + fn new() -> Arc { + Arc::new(Self(Mutex::new(vec![0xa5; 0x10000]))) + } + fn words(&self, at: usize, words: &[u32]) { + for (i, w) in words.iter().enumerate() { + self.0.lock()[at + i * 4..at + i * 4 + 4].copy_from_slice(&w.to_le_bytes()); + } + } + fn word(&self, at: usize) -> u32 { + u32::from_le_bytes(self.0.lock()[at..at + 4].try_into().unwrap()) + } +} +impl PciDma for Memory { + fn dma_read(&self, addr: u64, dst: &mut [u8]) -> PciDmaStatus { + match self.0.lock().get(addr as usize..addr as usize + dst.len()) { + Some(s) => { + dst.copy_from_slice(s); + PciDmaStatus::Ok + } + None => PciDmaStatus::MasterAbort, + } + } + fn dma_write(&self, addr: u64, src: &[u8]) -> PciDmaStatus { + match self.0.lock().get_mut(addr as usize..addr as usize + src.len()) { + Some(s) => { + s.copy_from_slice(src); + PciDmaStatus::Ok + } + None => PciDmaStatus::MasterAbort, + } + } +} + +/// An OHCI controller with the device on port 0, reset and enabled, with +/// the periodic list holding one interrupt-IN endpoint at `address`. +fn ohci_with(f: Arc, address: u8) -> (Arc, Arc) { + let h = Ohci::new(); + let m = Memory::new(); + h.connect_dma(m.clone()); + h.attach(0, f.clone()).unwrap(); + h.start(); + h.bar_write(0, 0x54, 0x100, 4); + h.bar_write(0, 0x54, 0x10, 4); + h.advance_frames(10); + // The port reset returned the device to its default state: enumerate it. + enumerate(&*f, address); + m.words(0x1000, &[0; 64]); + // Interrupt table: every slot points at the endpoint. ED: address, endpoint 1, + // direction from the TD, low speed, max packet 8; TD queue 0x3000 .. tail 0x3020. + m.words(0x1000, &[0x2000; 32]); + m.words(0x2000, &[u32::from(address) | (1 << 7) | (1 << 13) | (8 << 16), 0x3020, 0x3000, 0]); + // TD: direction IN (2), buffer 0x4000..0x4007, buffer rounding allowed, interrupt delay 0. + m.words(0x3000, &[0xf000_0000 | (2 << 19) | (1 << 18), 0x4000, 0x3020, 0x4007]); + h.config().write(1, 6); + h.bar_write(0, 0x18, 0x1000, 4); + h.bar_write(0, 0x04, 0x84, 4); + (h, m) +} + +/// A report waiting in the device is carried by an interrupt TD into guest +/// memory, with the TD retired as a success. +#[test] +fn an_interrupt_transfer_returns_a_keyboard_report_through_ohci() { + let k = UsbKeyboard::new(); + let (h, m) = ohci_with(k.clone(), 5); + k.key(KeyCode::KeyA, true); + h.advance_frames(2); + let report = m.0.lock()[0x4000..0x4009].to_vec(); + // The done queue is published once the delay for a low-priority completion passes. + h.advance_frames(10); + assert_eq!(report, vec![0, 0, 4, 0, 0, 0, 0, 0, 0xa5], "the report, and nothing past its end"); + assert_eq!(m.word(0x3000) >> 28, 0, "completed without error"); + assert_eq!(m.word(0x1084) & !15, 0x3000, "the TD is on the done queue"); +} + +/// With nothing to report the device NAKs and the transfer stays queued. +#[test] +fn an_interrupt_transfer_with_no_report_stays_pending() { + let m = UsbMouse::new(); + let (h, mem) = ohci_with(m.clone(), 5); + h.advance_frames(3); + assert_eq!(mem.word(0x3000) >> 28, 0xf, "not accessed"); + m.mouse(2, 1, 1, 0); + h.advance_frames(2); + assert_eq!(&mem.0.lock()[0x4000..0x4003], [0, 1, 1]); + assert_eq!(mem.word(0x3000) >> 28, 0); +} diff --git a/src/devlog.rs b/src/devlog.rs index 406858d4f..045cd6f4f 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -57,6 +57,7 @@ pub enum LogModule { Odyssey = 28, Bedrock = 29, L1 = 30, + Usb = 31, } // Mask bits shared by L1i / L1d / L2c @@ -65,7 +66,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 31; + pub const COUNT: usize = 32; pub fn from_str(s: &str) -> Option { match s { @@ -100,6 +101,7 @@ impl LogModule { "odyssey" => Some(Self::Odyssey), "bedrock" => Some(Self::Bedrock), "l1" => Some(Self::L1), + "usb" => Some(Self::Usb), _ => None, } } @@ -137,6 +139,7 @@ impl LogModule { Self::Odyssey => "odyssey", Self::Bedrock => "bedrock", Self::L1 => "l1", + Self::Usb => "usb", } } @@ -147,7 +150,7 @@ impl LogModule { Self::Vino, Self::Dcb, Self::Vc2, Self::Cmap, Self::Xmap, Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, - Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, Self::L1, + Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, Self::L1, Self::Usb, ] } @@ -291,6 +294,7 @@ impl DevLog { ModuleLog::new(), // Odyssey ModuleLog::new(), // Bedrock ModuleLog::new(), // L1 + ModuleLog::new(), // Usb ], } } From e4a72e0ee27d8a1b0dace73912d6eb2f9e9aadca Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 16/20] fuel: add the serial IP35 machine Build the Fuel IP34 board around its R14000, Bedrock, XBridge, IOC3 and dual-channel ISP12160. Use one Physical memory table with sparse processor and peripheral views, and retain the CPU memory fast paths after the PROM startup store retires. Uncached byte and halfword stores reach a device through BusDevice::write_narrow_from, which each window forwards at the device-local address; only the memory controller's ECC view reads more than the low bits of the register. With the jitv2 feature, start the compile pool after the memory windows are published, as the Octane and O2 do. Each worker copies the cache geometry once as it starts, and without the windows that geometry has no inline load or store path. Workers read code through the cached plane, which serves plain physical addresses; the uncached views would decode them as node registers. The machine describes where its memory can answer to the CPU: the four 1 GiB slots Bedrock's controller places its two 256 MiB banks in, and the reset PROM. Its disks on both ISP12160 channels are flushed at power-off, quit and exit like the other machines', so a copy-on-write overlay keeps its record of held sectors after a guest `halt -p`. The processor is R14000FuelShadowCache, the Octane's R14000 at revision 2.3 (PRId 0x0f23), the revision a public software inventory of a Fuel prints, where the Octane's part reports 2.4. The raw CP0 word, the FIR and the 128-byte lines are taken from the Octane part, not read from a Fuel. Like the Octane's R14000 it reports "R12000" as its name. The machine loads its PROM through prom_image. Feed the IOC3 PS/2 controller from the window, VNC and CI input, and let a USB keyboard on root port 0 and a USB mouse on port 1 share it. The Fuel's manual shows PS/2 as the standard keyboard and mouse, so ps2_input under [fuel] defaults on and usb_input defaults off; setting usb_input fits the USB pair, and ps2_input = false leaves the PS/2 pair out. The monitor's ps2 command reaches the keyboard and mouse too. PROM loader (prom_image): Add a device-independent loader for PROM images. It accepts a raw payload or a version-six stored or gzip container, decodes the recognized containers explicitly and reports malformed inputs with their path before any bytes reach guest memory. Config profile (config): The Fuel is a new machine profile with two memory banks, wide SCSI buses, no Ethernet adapter choice beyond its built-in one, and no graphics board by default. The [fuel] section selects the serial console (the L1 controller or either IOC3 port), the L1 TCP port and log, and the persistent boot flash store. l1_revision and brick_serial replace the L1's reported firmware revision (default 1.48.1) and brick serial (default EMUN00). ps2_input defaults on and usb_input off, as the Fuel's manual shows PS/2 as the standard keyboard and mouse; setting usb_input fits a USB keyboard and mouse, and ps2_input = false leaves the PS/2 pair out. The key names the part: machine.cpu = "r14000" (r10000, the family, is still accepted), and a Fuel config cannot claim another processor. The Octane's R14000 is revision 2.4; the Fuel's is 2.3. --- HELP.md | 6 +- src/config.rs | 260 +++++++- src/cpu/mips_cache_shadow.rs | 1 - src/cpu/parts.rs | 3 + src/dev/l1.rs | 5 + src/dev/mod.rs | 1 + src/dev/prom_image.rs | 210 +++++++ src/ip35.rs | 368 +++++++++++ src/ip35/tests.rs | 642 ++++++++++++++++++++ src/ip35_machine.rs | 1107 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/machine.rs | 8 +- src/workstation.rs | 1 + 13 files changed, 2593 insertions(+), 22 deletions(-) create mode 100644 src/dev/prom_image.rs create mode 100644 src/ip35.rs create mode 100644 src/ip35/tests.rs create mode 100644 src/ip35_machine.rs diff --git a/HELP.md b/HELP.md index b162ac278..049dd522a 100644 --- a/HELP.md +++ b/HELP.md @@ -1,7 +1,9 @@ -# IRIS — SGI Indy and Indigo2 emulator +# IRIS — SGI workstation emulator The complete core/GUI feature and configuration inventory is in -[FEATURES.md](FEATURES.md). This guide explains everyday use; the monitor's full command reference is in [debug.md](debug.md). +[FEATURES.md](FEATURES.md). For the Fuel, see the [Fuel guide](docs/fuel-ip34.md). +This guide explains everyday use. The monitor's full command reference is in +[debug.md](debug.md). ## Quick start diff --git a/src/config.rs b/src/config.rs index 6ec94facd..fdce2fc73 100644 --- a/src/config.rs +++ b/src/config.rs @@ -614,6 +614,8 @@ pub enum MachineProfile { /// R10000. Configured by the shared keys and the `[octane]` section; see /// `src/ip30_machine.rs` and `docs/Octane/ip30-address-map.md`. OctaneIp30, + /// Serial-only Fuel IP34 board using the IP35 software platform. + FuelIp34, } impl MachineProfile { @@ -636,6 +638,7 @@ impl MachineProfile { Self::OctaneIp30 => &[0, 64, 128, 256, 512, 1024, 2048], Self::O2Ip32 => &[0, 32, 64, 128, 256], Self::Indigo2Ip28 => MC_BANK_SIZES, + Self::FuelIp34 => &[0, 256], _ => &[0, 8, 16, 32, 64, 128], } } @@ -659,12 +662,13 @@ impl MachineProfile { /// Every profile, in the order `workstation::build` lists them. `ALL` is /// only the ones the GUI offers. - pub const EVERY: [Self; 5] = [ + pub const EVERY: [Self; 6] = [ Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28, Self::O2Ip32, Self::OctaneIp30, + Self::FuelIp34, ]; /// The name `[machine] profile` and `--machine` give this profile. @@ -675,6 +679,7 @@ impl MachineProfile { Self::Indigo2Ip28 => "indigo2_ip28", Self::O2Ip32 => "o2_ip32", Self::OctaneIp30 => "octane_ip30", + Self::FuelIp34 => "fuel_ip34", } } @@ -729,6 +734,7 @@ impl MachineProfile { Self::Indigo2Ip28 => "SGI Indigo2 IMPACT (IP28)", Self::O2Ip32 => "SGI O2 (IP32)", Self::OctaneIp30 => "SGI Octane (IP30)", + Self::FuelIp34 => "SGI Fuel (IP34)", } } @@ -738,7 +744,7 @@ impl MachineProfile { /// connector), one on the Indy. pub fn scsi_controllers(self) -> u8 { match self { - Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 => 2, + Self::Indigo2Ip22 | Self::O2Ip32 | Self::OctaneIp30 | Self::FuelIp34 => 2, _ => 1, } } @@ -747,18 +753,19 @@ impl MachineProfile { /// buses are wide, with ids 0 to 15 and the controller on 0. The Indy's /// and Indigo2's WD33C93A chips are narrow, with ids 0 to 7. The O2's /// Adaptec buses are wide too, but stay at 7 until they are widened on - /// purpose. + /// purpose. The Fuel's ISP12160 buses are wide. pub fn max_scsi_id(self) -> u8 { match self { - Self::OctaneIp30 => 15, + Self::OctaneIp30 | Self::FuelIp34 => 15, _ => 7, } } - /// How many memory banks the machine has: four on all of them (the MC's - /// four banks; the O2's four DIMM pairs). `banks` has room for more. + /// How many memory banks the machine has: four on the MC, HEART and O2 + /// (the O2's four DIMM pairs), two on Fuel. `banks` has room for more; + /// unused entries must be zero. pub fn memory_banks(self) -> usize { - 4 + if self == Self::FuelIp34 { 2 } else { 4 } } pub fn guinness(self) -> bool { @@ -772,6 +779,7 @@ impl MachineProfile { Self::OctaneIp30 => crate::ip30_machine::CPUS, Self::O2Ip32 => crate::ip32_machine::CPUS, Self::Indigo2Ip28 => crate::machine::IP28_CPUS, + Self::FuelIp34 => crate::ip35_machine::CPUS, Self::IndyIp24 | Self::Indigo2Ip22 => crate::machine::CPUS, } } @@ -822,7 +830,7 @@ pub enum GraphicsBoard { #[clap(name = "v12", alias = "vpro:v12", alias = "vpro_v12")] V12, /// No graphics board: the machine runs on its serial console. Octane - /// only. + /// and Fuel. #[clap(name = "none")] None, } @@ -884,6 +892,7 @@ impl GraphicsBoard { pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, + MachineProfile::FuelIp34 => self == Self::None, MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -893,6 +902,7 @@ impl GraphicsBoard { pub fn default_for(profile: MachineProfile) -> Self { match profile { MachineProfile::OctaneIp30 => Self::SolidImpact, + MachineProfile::FuelIp34 => Self::None, _ => Self::Newport, } } @@ -952,7 +962,7 @@ impl std::fmt::Display for GraphicsBoard { #[serde(deny_unknown_fields)] pub struct GraphicsSection { /// Graphics board: Newport, GR2 XZ/Extreme, IMPACT Solid/High/Maximum, - /// or none (Octane only). Unset: the machine's default + /// or none (Octane and Fuel). Unset: the machine's default /// ([`GraphicsBoard::default_for`]); read it with /// [`MachineConfig::graphics_board`]. Extreme is restricted to IP22, /// the Octane to IMPACT or none; non-Newport boards require one head @@ -1668,6 +1678,80 @@ mod scsi_keys { } } +/// Fuel's independent serial connections. The selected console serves CI and serial_log. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FuelConsole { + #[default] + L1, + Ioc3A, + Ioc3B, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FuelSection { + /// Persistent 16 MiB boot flash store; absent means volatile flash. + #[serde(default)] + pub flash: Option, + #[serde(default)] + pub console: FuelConsole, + /// L1 host console TCP port; IOC3 keeps serial_port_a and serial_port_b. + #[serde(default)] + pub l1_port: Option, + /// Separate L1 log when an IOC3 port is selected as console. + #[serde(default)] + pub l1_log: Option, + /// Fit the PS/2 keyboard and mouse controller in the onboard IOC3 and + /// feed it the host's keyboard and mouse. + #[serde(default = "default_fuel_input")] + pub ps2_input: bool, + /// Plug a USB keyboard and mouse into the OHCI root ports and feed them + /// the same host input. Off by default: the Fuel's manual and the PROM + /// treat PS/2 as the standard keyboard and mouse. + #[serde(default)] + pub usb_input: bool, + /// L1 firmware revision as "major.minor.patch", reported to request + /// 0x0005. Unset: 1.48.1. + #[serde(default)] + pub l1_revision: Option, + /// Brick serial reported to L1 request 0x0004, at most 37 bytes. Unset: + /// the marked synthetic `EMUN00`. + #[serde(default)] + pub brick_serial: Option, +} + +fn default_fuel_input() -> bool { true } + +impl Default for FuelSection { + fn default() -> Self { + Self { + flash: None, + console: FuelConsole::default(), + l1_port: None, + l1_log: None, + ps2_input: true, + usb_input: false, + l1_revision: None, + brick_serial: None, + } + } +} + +impl FuelSection { + /// Left out of a written config when nothing in it was set. + fn is_default(&self) -> bool { + self.flash.is_none() + && self.console == FuelConsole::default() + && self.l1_port.is_none() + && self.l1_log.is_none() + && self.ps2_input + && !self.usb_input + && self.l1_revision.is_none() + && self.brick_serial.is_none() + } +} + /// Top-level machine configuration. /// /// Field order matters for TOML export: the `toml` serializer requires every @@ -1916,6 +2000,10 @@ pub struct MachineConfig { #[serde(default, skip_serializing_if = "OctaneSection::is_default")] pub octane: OctaneSection, + /// Serial console and flash storage for the Fuel. + #[serde(default, skip_serializing_if = "FuelSection::is_default")] + pub fuel: FuelSection, + /// Host performance tuning (`[perf]` section). #[serde(default)] pub perf: PerfConfig, @@ -2144,6 +2232,7 @@ impl Default for MachineConfig { jitv2: Jitv2Config::default(), o2: O2Section::default(), octane: OctaneSection::default(), + fuel: FuelSection::default(), perf: PerfConfig::default(), clock: ClockConfig::default(), rtc_offset: RtcOffset::default(), @@ -2193,6 +2282,9 @@ impl MachineConfig { } // The Octane has a PCI bridge with a free slot. MachineProfile::OctaneIp30 => Ok(()), + MachineProfile::FuelIp34 => Err( + "[virtio_serial] is not wired on the Fuel: its XBridge has no virtio-pci attachment".into(), + ), MachineProfile::O2Ip32 => Err( "[virtio_serial] is not wired on the O2 yet: the virtio-pci transport exists, but the \ MACE PCI bridge has no interrupt route for a third slot (see docs/virtio-serial.md)" @@ -2342,7 +2434,7 @@ impl MachineConfig { /// Validate bank sizes, returns a description of any errors. /// The graphics board: `[graphics] board`, or the machine's default - /// when it is not set (Solid IMPACT on the Octane, Newport elsewhere). + /// (Solid IMPACT on Octane, none on Fuel, Newport elsewhere). pub fn graphics_board(&self) -> GraphicsBoard { self.graphics.board.unwrap_or_else(|| GraphicsBoard::default_for(self.machine.profile)) } @@ -2431,9 +2523,9 @@ impl MachineConfig { "scsi.{} and scsi.{} both name {}", other, key, scsi_label(controller, id) )); } - if dev.is_daynaport() && self.machine.profile == MachineProfile::O2Ip32 { + if dev.is_daynaport() && matches!(self.machine.profile, MachineProfile::O2Ip32 | MachineProfile::FuelIp34) { return Err(format!( - "scsi.{}: kind = \"daynaport\" is not available on the O2 (its Ethernet is built in)", key + "scsi.{}: kind = \"daynaport\" is not available on {} (its Ethernet is built in)", key, self.machine.profile.label() )); } if dev.is_daynaport() && self.network.mode == NetMode::Stream { @@ -2468,6 +2560,19 @@ impl MachineConfig { } } crate::dev::crime_mem::check_kits(&self.o2_kits()).map_err(|e| format!("banks: {e}"))?; + } else if self.machine.profile == MachineProfile::FuelIp34 { + if self.banks[..2] != [256, 256] { + return Err("Fuel currently requires banks = [256, 256] (512 MiB)".into()); + } + if board != GraphicsBoard::None { + return Err("Fuel currently supports graphics.board = \"none\" only".into()); + } + if let Some(text) = &self.fuel.l1_revision { + crate::dev::l1::parse_revision(text).map_err(|e| format!("fuel.l1_revision: {e}"))?; + } + if self.fuel.brick_serial.as_ref().is_some_and(|s| s.len() > 37 || s.contains('\0')) { + return Err("fuel.brick_serial must be at most 37 bytes with no NUL".into()); + } } else if self.machine.profile == MachineProfile::OctaneIp30 { // A HEART bank is a matched DIMM pair: 64 MB to 2 GB, filled from // bank 0 without a gap, 8 GB at most (SGI's memory upgrade guide). @@ -2513,8 +2618,8 @@ impl MachineConfig { } } match (self.network.mode, self.machine.profile) { - (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 => { - return Err("network.mode = \"off\" is only available on the O2 and the Octane for now; the Indy and Indigo2 always have their Ethernet".into()); + (NetMode::Off, p) if p != MachineProfile::O2Ip32 && p != MachineProfile::OctaneIp30 && p != MachineProfile::FuelIp34 => { + return Err("network.mode = \"off\" is only available on the O2, Octane and Fuel; the Indy and Indigo2 always have their Ethernet".into()); } (NetMode::Stream, _) => { let ep = self.network.stream.as_deref() @@ -3578,7 +3683,7 @@ mod export_tests { assert_eq!(apply(&["--ip22"]).machine.profile, MachineProfile::Indigo2Ip22); assert!(Cli::try_parse_from(["iris", "--ip22", "--machine", "o2_ip32"]).is_err()); let e = MachineProfile::from_name("vax").unwrap_err(); - assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30)"); + assert_eq!(e, "unknown machine \"vax\" (valid: indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32, octane_ip30, fuel_ip34)"); let list = MachineProfile::list(); assert_eq!(list.lines().count(), MachineProfile::EVERY.len()); assert!(list.starts_with("indy_ip24 SGI Indy (IP24)\n"), "{list}"); @@ -3587,7 +3692,7 @@ mod export_tests { for p in MachineProfile::EVERY { match p { MachineProfile::IndyIp24 | MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28 - | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 => {} + | MachineProfile::O2Ip32 | MachineProfile::OctaneIp30 | MachineProfile::FuelIp34 => {} } let cfg: MachineConfig = toml::from_str(&format!("[machine]\nprofile = \"{}\"", p.name())).unwrap(); assert_eq!(cfg.machine.profile, p); @@ -3602,6 +3707,7 @@ mod export_tests { let accepts = |p: MachineProfile, cpu: &str| p.part(Some(cpu)).is_ok(); assert!(accepts(MachineProfile::OctaneIp30, "r14000") && !accepts(MachineProfile::OctaneIp30, "r5000")); assert!(accepts(MachineProfile::IndyIp24, "r5000") && !accepts(MachineProfile::IndyIp24, "r10000")); + assert!(accepts(MachineProfile::FuelIp34, "r14000") && !accepts(MachineProfile::FuelIp34, "r10000")); // The counts are what the checks in validate() and the Octane // constructor allowed before they moved here: one on every machine @@ -4034,3 +4140,125 @@ mod ci_overlay_tests { assert!(a.contains(&format!("iris-ci-{}-", std::process::id())), "{a}"); } } + +#[cfg(test)] +mod fuel_input_tests { + use super::*; + + /// A config saved for another machine must not grow a `[fuel]` table, + /// and a Fuel's own settings must survive a save and reload. + #[test] + fn only_a_configured_fuel_section_is_saved_and_it_round_trips() { + let other = MachineConfig::default(); + let saved = toml::to_string(&other).unwrap(); + assert!(!saved.contains("[fuel]"), "{saved}"); + let mut fuel = MachineConfig::default(); + fuel.fuel.console = FuelConsole::Ioc3B; + fuel.fuel.flash = Some("flash.bin".into()); + fuel.fuel.l1_port = Some(18882); + fuel.fuel.l1_log = Some("l1.log".into()); + fuel.fuel.usb_input = true; + fuel.fuel.ps2_input = false; + let saved = toml::to_string(&fuel).unwrap(); + assert!(saved.contains("[fuel]"), "{saved}"); + let loaded: MachineConfig = toml::from_str(&saved).unwrap(); + assert_eq!(loaded.fuel.console, FuelConsole::Ioc3B); + assert_eq!(loaded.fuel.flash.as_deref(), Some("flash.bin")); + assert_eq!(loaded.fuel.l1_port, Some(18882)); + assert_eq!(loaded.fuel.l1_log.as_deref(), Some("l1.log")); + assert!(!loaded.fuel.ps2_input && loaded.fuel.usb_input); + assert!(!MachineProfile::ALL.contains(&MachineProfile::FuelIp34)); + } + + /// PS/2 is the Fuel's standard keyboard and mouse; USB is opt-in, and a + /// `[fuel]` table that names neither key keeps both defaults. + #[test] + fn fuel_input_defaults_to_ps2_with_usb_opt_in() { + let none: MachineConfig = toml::from_str("[fuel]\nflash = \"flash.bin\"\n").unwrap(); + assert!(none.fuel.ps2_input && !none.fuel.usb_input); + assert!(MachineConfig::default().fuel.is_default()); + let usb: MachineConfig = toml::from_str("[fuel]\nusb_input = true\n").unwrap(); + assert!(usb.fuel.ps2_input && usb.fuel.usb_input); + assert!(!usb.fuel.is_default(), "an opt-in is written back"); + } + + /// The Fuel's processor is fixed, so `machine.cpu` may only name it (or the + /// family, as the key did before the R14000 had a name); the default + /// `r4400` is refused rather than silently ignored. + /// The L1 identity keys parse, validate and round trip; unset they are + /// left out of a written config. + #[test] + fn fuel_l1_identity_keys_are_validated_and_round_trip() { + let base = "banks = [256, 256]\n[machine]\nprofile = \"fuel_ip34\"\ncpu = \"r14000\"\n"; + let ok: MachineConfig = toml::from_str(&format!( + "{base}[fuel]\nl1_revision = \"1.50.2\"\nbrick_serial = \"EMUN03\"\n" + )).unwrap(); + ok.validate().unwrap(); + let saved = toml::to_string(&ok).unwrap(); + let back: MachineConfig = toml::from_str(&saved).unwrap(); + assert_eq!(back.fuel.l1_revision.as_deref(), Some("1.50.2")); + assert_eq!(back.fuel.brick_serial.as_deref(), Some("EMUN03")); + let plain: MachineConfig = toml::from_str(base).unwrap(); + plain.validate().unwrap(); + assert!(plain.fuel.l1_revision.is_none() && plain.fuel.brick_serial.is_none()); + let bad_rev: MachineConfig = toml::from_str(&format!("{base}[fuel]\nl1_revision = \"1.48\"\n")).unwrap(); + assert!(bad_rev.validate().unwrap_err().contains("fuel.l1_revision")); + let long = "x".repeat(38); + let bad_serial: MachineConfig = toml::from_str(&format!("{base}[fuel]\nbrick_serial = \"{long}\"\n")).unwrap(); + assert!(bad_serial.validate().unwrap_err().contains("fuel.brick_serial")); + } + + #[test] + fn fuel_takes_only_its_r14000() { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::FuelIp34; + cfg.banks.fill(0); + cfg.banks[0] = 256; + cfg.banks[1] = 256; + cfg.scsi.clear(); + // Leaving `cpu` out takes the Fuel's own part. + cfg.machine.cpu = None; + cfg.validate().unwrap(); + assert_eq!(cfg.cpu(), &crate::cpu::parts::R14000_500); + for name in ["r14000", "r14000_500"] { + cfg.machine.cpu = Some(name.into()); + cfg.validate().unwrap(); + assert_eq!(cfg.cpu(), &crate::cpu::parts::R14000_500, "{name}"); + } + for name in ["r10000", "r12000_360", "r14000_600"] { + cfg.machine.cpu = Some(name.into()); + assert!(cfg.validate().unwrap_err().contains("r14000_500"), "{name}"); + } + } + + #[test] + fn fuel_requires_its_fixed_memory_population_and_no_graphics() { + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::FuelIp34; + cfg.banks.fill(0); + cfg.banks[0] = 256; + cfg.banks[1] = 256; + cfg.scsi.clear(); + assert_eq!(cfg.graphics_board(), GraphicsBoard::None); + cfg.network.mode = NetMode::Off; + cfg.validate().unwrap(); + cfg.network.mode = NetMode::Pcap; + cfg.validate().unwrap(); + cfg.graphics.board = Some(GraphicsBoard::Newport); + assert!(cfg.validate().unwrap_err().contains("none")); + cfg.graphics.board = None; + cfg.banks[1] = 0; + assert!(cfg.validate().unwrap_err().contains("256, 256")); + } + + #[test] + fn fuel_accepts_wide_scsi_ids_on_both_channels_without_changing_narrow_profiles() { + let text = "banks = [256, 256]\n[machine]\nprofile = \"fuel_ip34\"\ncpu = \"r14000\"\n[scsi.15]\npath = \"disk.raw\"\n[scsi.31]\npath = \"disk1.raw\"\n"; + let mut cfg: MachineConfig = toml::from_str(text).unwrap(); + cfg.validate().unwrap(); + // The Octane's buses are wide as well; the Indy's WD33C93A is narrow. + cfg.machine.profile = MachineProfile::IndyIp24; + cfg.machine.cpu = Some("r4400".into()); + assert!(cfg.validate().unwrap_err().contains("out of range")); + } +} diff --git a/src/cpu/mips_cache_shadow.rs b/src/cpu/mips_cache_shadow.rs index c50605acc..e8f0e52fd 100644 --- a/src/cpu/mips_cache_shadow.rs +++ b/src/cpu/mips_cache_shadow.rs @@ -788,7 +788,6 @@ pub type R12000ShadowCache = pub type R12000O2ShadowCache = ShadowCache<32768, 64, 32768, 32, 2097152, 64, true, 0x0000_0e35, 0x0000_0900, 64, { model::R12000 }>; - /// SGI O2 R10000 (IP32): the IP28 model with 64-byte secondary-cache lines. /// /// The O2 PROM (4.18 and earlier) handles only that line size. With Config's diff --git a/src/cpu/parts.rs b/src/cpu/parts.rs index 598b923a8..dc9a1c132 100644 --- a/src/cpu/parts.rs +++ b/src/cpu/parts.rs @@ -48,6 +48,9 @@ pub const R10000_250: Part = Part { mhz: 250, rev: (3, 4), ..R10000_195 }; pub const R12000_360: Part = Part { family: Family::R12000, mhz: 360, rev: (3, 5), l2_kb: 2048 }; pub const R12000_400: Part = Part { mhz: 400, ..R12000_360 }; pub const R14000_600: Part = Part { family: Family::R14000, mhz: 600, rev: (2, 4), l2_kb: 2048 }; +/// The Fuel's R14000A: rev 2.3 at 500 MHz with a 2 MB secondary cache, as the IP35 +/// `hinv` dumps show it. +pub const R14000_500: Part = Part { mhz: 500, rev: (2, 3), ..R14000_600 }; /// Every part, for the bare-metal bench rig, which takes any. A name matches /// the first listed, so the rig runs the later silicon of a clock that has two. diff --git a/src/dev/l1.rs b/src/dev/l1.rs index c23b6c256..c09e9f6a6 100644 --- a/src/dev/l1.rs +++ b/src/dev/l1.rs @@ -192,6 +192,11 @@ impl L1 { self.state.lock().config.mac } + #[cfg(test)] + pub fn config(&self) -> L1Config { + self.state.lock().config.clone() + } + #[cfg(test)] pub fn display_line(&self) -> String { self.state.lock().display.clone() diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 33c4182d3..c769b803e 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -74,5 +74,6 @@ pub mod ics1890; pub mod isp12160; pub mod l1; pub mod ohci; +pub mod prom_image; pub mod timekeeper; pub mod xbridge; diff --git a/src/dev/prom_image.rs b/src/dev/prom_image.rs new file mode 100644 index 000000000..1a50f89b9 --- /dev/null +++ b/src/dev/prom_image.rs @@ -0,0 +1,210 @@ +//! SGI PROM image loading: version-six single-segment containers and raw +//! payloads. A recognized but malformed container is rejected rather than +//! retried as a raw image. +//! +//! Ported from hw/mips/sgi/prom-image.c in jamesbraid/qemu-sgi-origami. + +use std::io::{self, Read}; + +fn invalid(message: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} +fn word(data: &[u8], off: usize) -> u64 { + u64::from_be_bytes(data[off..off + 8].try_into().unwrap()) +} +fn byte_sum(data: &[u8]) -> u64 { + data.iter().map(|b| *b as u64).sum() +} + +/// Read a PROM file and decode its payload, retaining the path in errors. +pub fn load(path: &std::path::Path, payload_limit: usize) -> io::Result> { + let with_path = |error: io::Error| { + io::Error::new(error.kind(), format!("PROM image {}: {error}", path.display())) + }; + let file = std::fs::File::open(path).map_err(with_path)?; + // Bound the encoded file by zlib's compression bound, the container + // header, gzip framing and the accepted zero tail. + let size = payload_limit as u64; + let file_limit = size + .saturating_add(size >> 12) + .saturating_add(size >> 14) + .saturating_add(size >> 25) + .saturating_add(13 + 4096 + 12 + 7); + if file.metadata().map_err(with_path)?.len() > file_limit { + return Err(with_path(invalid( + "encoded PROM image exceeds flash capacity", + ))); + } + let mut bytes = Vec::new(); + file.take(file_limit.saturating_add(1)) + .read_to_end(&mut bytes) + .map_err(with_path)?; + if bytes.len() as u64 > file_limit { + return Err(with_path(invalid( + "encoded PROM image exceeds flash capacity", + ))); + } + decode(&bytes, payload_limit).map_err(with_path) +} + +/// Accept raw payload bytes or the supported stored/gzip container subset. +/// Gzip output is bounded before decoding, and exactly one member is required. +pub fn decode(file: &[u8], payload_limit: usize) -> io::Result> { + if file.len() < 0x48 || word(file, 0x40) != 0x4a464b535743534d { + if file.len() > payload_limit { + return Err(invalid("PROM image exceeds flash capacity")); + } + return Ok(file.to_vec()); + } + if file.len() < 4096 { + return Err(invalid("truncated SGI PROM container header")); + } + if word(file, 0x48) != 6 { + return Err(invalid("unsupported SGI PROM container version")); + } + if word(file, 0x50) != file.len() as u64 { + return Err(invalid("SGI PROM declared end does not match file size")); + } + if u32::from_be_bytes(file[0x7c..0x80].try_into().unwrap()) != 1 { + return Err(invalid("unsupported SGI PROM segment count")); + } + if word(file, 0x98) != 4096 { + return Err(invalid("unsupported SGI PROM payload offset")); + } + let flags = word(file, 0x90); + if flags != 0x10 && flags != 0x13 { + return Err(invalid("unsupported SGI PROM flags")); + } + let length = word(file, 0xb0); + if length == 0 || length > payload_limit as u64 { + return Err(invalid( + "SGI PROM decoded length is zero or exceeds capacity", + )); + } + let payload = if flags == 0x10 { + if word(file, 0xb8) != 0 || word(file, 0xc8) != 0 || word(file, 0xd0) != length { + return Err(invalid("unsupported SGI PROM uncompressed metadata")); + } + if length != (file.len() - 4096) as u64 { + return Err(invalid("invalid SGI PROM stored length")); + } + file[4096..].to_vec() + } else { + let stored = word(file, 0xb8); + if stored == 0 || stored > (file.len() - 4096) as u64 { + return Err(invalid("invalid SGI PROM stored length")); + } + let end = 4096 + stored as usize; + if file.len() - end > 7 || file[end..].iter().any(|b| *b != 0) { + return Err(invalid("unsupported SGI PROM trailing bytes")); + } + if byte_sum(&file[4096..end]) != word(file, 0xc8) { + return Err(invalid("SGI PROM stored checksum mismatch")); + } + let mut decoder = flate2::bufread::GzDecoder::new(&file[4096..end]); + let mut decoded = Vec::new(); + (&mut decoder).take(length + 1).read_to_end(&mut decoded)?; + if decoded.len() as u64 != length || !decoder.get_ref().is_empty() { + return Err(invalid("invalid SGI PROM gzip segment or decoded length")); + } + decoded + }; + if byte_sum(&payload) != word(file, 0xc0) { + return Err(invalid("SGI PROM decoded checksum mismatch")); + } + Ok(payload) +} + +#[cfg(test)] +mod tests { + // Cases follow tests/unit/test-sgi-prom-image.c in + // jamesbraid/qemu-sgi-origami: wrapped, gzip, raw, malformed and files. + use super::*; + use std::io::Write; + fn put(v: &mut [u8], off: usize, n: u64) { + v[off..off + 8].copy_from_slice(&n.to_be_bytes()); + } + fn container(payload: &[u8], gzip: bool) -> Vec { + let stored = if gzip { + let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + enc.write_all(payload).unwrap(); + enc.finish().unwrap() + } else { + payload.to_vec() + }; + let mut v = vec![0; 4096]; + put(&mut v, 0x40, 0x4a464b535743534d); + put(&mut v, 0x48, 6); + put(&mut v, 0x50, (4096 + stored.len()) as u64); + v[0x7c..0x80].copy_from_slice(&1u32.to_be_bytes()); + put(&mut v, 0x90, if gzip { 0x13 } else { 0x10 }); + put(&mut v, 0x98, 4096); + put(&mut v, 0xb0, payload.len() as u64); + put(&mut v, 0xc0, byte_sum(payload)); + if gzip { + put(&mut v, 0xb8, stored.len() as u64); + put(&mut v, 0xc8, byte_sum(&stored)); + } else { + put(&mut v, 0xd0, payload.len() as u64); + } + v.extend(stored); + v + } + /// Equivalent PROM payloads must boot from each supported image format. + #[test] + fn raw_stored_and_gzip_images_load_with_the_same_capacity_limit() { + let path = std::env::temp_dir().join(format!("iris-prom-load-{}", std::process::id())); + for file in [vec![9, 8, 7, 6], container(&[9, 8, 7, 6], false), container(&[9, 8, 7, 6], true)] { + std::fs::write(&path, file).unwrap(); + assert_eq!(load(&path, 4).unwrap(), [9, 8, 7, 6]); + assert!(load(&path, 3).is_err(), "decoded payload exceeds capacity"); + } + std::fs::write(&path, []).unwrap(); + assert!(load(&path, 4).unwrap().is_empty()); + std::fs::remove_file(path).unwrap(); + } + + /// An invalid PROM must identify its input file instead of executing a + /// malformed container. + #[test] + fn invalid_or_unavailable_images_report_their_path() { + let path = std::env::temp_dir().join(format!("iris-prom-invalid-{}", std::process::id())); + let error = load(&path, 4).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert!(error.to_string().contains(&path.display().to_string())); + let reject = |file: &[u8]| { + std::fs::write(&path, file).unwrap(); + let error = load(&path, 4).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert!(error.to_string().contains(&path.display().to_string())); + }; + reject(&vec![0; 5000]); // encoded file bound + let original = container(&[4, 3, 2, 1], false); + reject(&original[..100]); // recognized but truncated header + for (off, n) in [(0x48, 7), (0x50, 4096), (0x90, 0x14), (0x98, 4095), (0xb0, 0), (0xd0, 3)] { + let mut file = original.clone(); + put(&mut file, off, n); + reject(&file); + } + for gzip in [false, true] { + let mut file = container(&[4, 3, 2, 1], gzip); + file[0xc7] ^= 1; + reject(&file); + } + let gzip = container(&[4, 3, 2, 1], true); + for tail in [7, 8] { + let mut file = gzip.clone(); + file.extend(vec![0; tail]); + let size = file.len(); + put(&mut file, 0x50, size as u64); + if tail == 7 { + std::fs::write(&path, &file).unwrap(); + assert_eq!(load(&path, 4).unwrap(), [4, 3, 2, 1]); + } else { + reject(&file); + } + } + std::fs::remove_file(path).unwrap(); + } + +} diff --git a/src/ip35.rs b/src/ip35.rs new file mode 100644 index 000000000..ff2af349f --- /dev/null +++ b/src/ip35.rs @@ -0,0 +1,368 @@ +//! The IP35 processor bus: Bedrock's four XKPHYS access-space views. +//! +//! Raw address bits 58:57 select Hub-special, I/O, fetch operations or uncached +//! RAM. Each view has a small fixed set of MMIO apertures, decoded by sorted +//! windows rather than a full physical-address table. Maps replace whole 4 KiB +//! pages as Physical does, while each endpoint still rejects padding and +//! enforces its cell permission. +//! +//! Cached RAM alone uses Physical, including its direct bank pointers, ppmem +//! and generation mappings. The MMIO views and register sub-bus need none of +//! those RAM facilities. Giving each its own full table would consume several +//! entries in the process-wide fault-handler registry per machine. +//! +//! Ported from hw/mips/sgi/sn1-node.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::bedrock_mem::{BANK_SPAN, FITTED_BANK}; +use crate::physical::Physical; +use crate::traits::*; +#[cfg(feature = "jitv2")] +use std::sync::{atomic::AtomicU64, OnceLock}; +use std::sync::Arc; + +pub(crate) const CPU_PHYS_MASK: u64 = ((1u64 << 40) - 1) | (3u64 << 57); +const PA_MASK: u64 = (1u64 << 40) - 1; + +/// Where the Fuel has memory: four 1 GiB slots Bedrock's memory controller +/// can place its two 256 MiB banks in, depending on how the PROM programs +/// it, and the reset PROM between the first two. Uncached fetches of the +/// PROM's own code land in its range. +pub fn memory_map() -> crate::memory_map::MemoryMap { + use crate::memory_map::{MemKind, MemRange, MemoryMap}; + const NAMES: [&str; 4] = ["ram slot 0", "ram slot 1", "ram slot 2", "ram slot 3"]; + let mut ranges: Vec = NAMES.iter().enumerate().map(|(slot, &name)| { + let base = slot as u64 * BANK_SPAN; + MemRange::new(name, MemKind::Ram, base, base + FITTED_BANK) + }).collect(); + ranges.push(MemRange::new("prom", MemKind::Rom, 0x1fc0_0000, 0x2000_0000)); + MemoryMap::new(ranges) +} + +struct Unclaimed; +impl BusDevice for Unclaimed {} + +/// An exact-bounds chip-relative window, optionally restricted by a cell permission. +/// Physical rounds maps to pages, so the adapter also refuses their padding. +pub(crate) struct Window { + base: u64, + size: u64, + device: Arc, + permission: Option bool + Send + Sync>>, +} +impl Window { + pub(crate) fn new(base: u64, size: u64, device: Arc) -> Self { + Self { base, size, device, permission: None } + } + pub(crate) fn permitted(mut self, permission: Arc bool + Send + Sync>) -> Self { + self.permission = Some(permission); + self + } + pub(crate) fn contains(&self, a: u64, n: u64) -> bool { self.local(a, n).is_some() } + fn local(&self, a: u64, n: u64) -> Option { + let local = a.checked_sub(self.base)?; + (local.checked_add(n)? <= self.size + && self.permission.as_ref().is_none_or(|allowed| allowed())) + .then_some(local) + } + fn write_masked_bytes(&self, a: u64, val: u64, mask: u64) -> u32 { + for i in 0..8usize { + let shift = (7 - i) * 8; + if (mask >> shift) & 0xff == 0 { + continue; + } + let Some(local) = self.local(a + i as u64, 1) else { + return BUS_ERR; + }; + let status = self.device.write8(local, (val >> shift) as u8); + if status != BUS_OK { + return status; + } + } + BUS_OK + } +} +macro_rules! window_access { + ($r:ident,$w:ident,$t:ty,$out:ident,$n:expr) => { + fn $r(&self, a: u64) -> $out { + self.local(a, $n).map(|at| self.device.$r(at)).unwrap_or_else($out::err) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.local(a, $n).map(|at| self.device.$w(at, v)).unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for Window { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + self.local(address, 1).map(|local| self.device.gen_ptr(local)).unwrap_or(std::ptr::null()) + } + fn write64_masked(&self, a: u64, val: u64, mask: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + if mask == 0 { + return BUS_OK; + } + if let Some(local) = self.local(a, 8) { + self.device.write64_masked(local, val, mask) + } else { + self.write_masked_bytes(a, val, mask) + } + } + fn write_narrow_from(&self, a: u64, size: usize, gpr: u64) -> u32 { + self.local(a, size as u64) + .map(|local| self.device.write_narrow_from(local, size, gpr)) + .unwrap_or(BUS_ERR) + } + window_access!(read8, write8, u8, BusRead8, 1); + window_access!(read16, write16, u16, BusRead16, 2); + window_access!(read32, write32, u32, BusRead32, 4); + window_access!(read64, write64, u64, BusRead64, 8); +} +pub(crate) fn map_device( + view: &Physical, + base: u64, + size: u64, + device: Arc, + name: &str, +) { + assert!(size > 0 && base.checked_add(size).is_some_and(|end| end <= PA_MASK + 1)); + view.map(base, base + size, Arc::new(Window::new(base, size, device)), name); +} + +struct DecodeRange { + start: u64, + end: u64, + window: Arc, +} + +/// Small MMIO maps have no RAM, direct pointers or host fault-handler entries. +/// Last map wins over whole pages, including padding rejected by its Window. +#[derive(Default)] +pub(crate) struct SparseWindowMap { + ranges: Vec, +} +impl SparseWindowMap { + pub(crate) fn map(&mut self, base: u64, size: u64, device: Arc) { + const PAGE: u64 = 1 << crate::ppmem::devmap::SLOT_SHIFT; + let end = base.checked_add(size).filter(|&end| size != 0 && end <= PA_MASK + 1) + .expect("IP35 window exceeds physical address space"); + let start = base & !(PAGE - 1); + let end = end.next_multiple_of(PAGE); + let window = Arc::new(Window::new(base, size, device)); + let mut ranges = Vec::with_capacity(self.ranges.len() + 2); + for old in self.ranges.drain(..) { + if old.end <= start || old.start >= end { + ranges.push(old); + } else { + if old.start < start { + ranges.push(DecodeRange { start: old.start, end: start, window: old.window.clone() }); + } + if old.end > end { + ranges.push(DecodeRange { start: end, end: old.end, window: old.window }); + } + } + } + ranges.push(DecodeRange { start, end, window }); + ranges.sort_unstable_by_key(|range| range.start); + self.ranges = ranges; + } + fn target(&self, address: u64) -> Option<&Window> { + let i = self.ranges.partition_point(|range| range.start <= address).checked_sub(1)?; + let range = &self.ranges[i]; + (address < range.end).then_some(range.window.as_ref()) + } +} +macro_rules! sparse_access { + ($read:ident,$write:ident,$ty:ty,$result:ident) => { + fn $read(&self, address: u64) -> $result { + self.target(address).map(|window| window.$read(address)).unwrap_or_else($result::err) + } + fn $write(&self, address: u64, value: $ty) -> u32 { + self.target(address).map(|window| window.$write(address, value)).unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for SparseWindowMap { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + self.target(address).map(|window| window.gen_ptr(address)).unwrap_or(std::ptr::null()) + } + fn write64_masked(&self, address: u64, value: u64, mask: u64) -> u32 { + self.target(address).map(|window| window.write64_masked(address, value, mask)) + .unwrap_or_else(|| Unclaimed.write64_masked(address, value, mask)) + } + fn write_narrow_from(&self, address: u64, size: usize, gpr: u64) -> u32 { + self.target(address).map(|window| window.write_narrow_from(address, size, gpr)) + .unwrap_or(BUS_ERR) + } + sparse_access!(read8, write8, u8, BusRead8); + sparse_access!(read16, write16, u16, BusRead16); + sparse_access!(read32, write32, u32, BusRead32); + sparse_access!(read64, write64, u64, BusRead64); +} + +/// The four uncached processor views decode MMIO separately from cached RAM. +pub struct Ip35Bus { + pub phys: Arc, + views: [SparseWindowMap; 4], + #[cfg(feature = "jitv2")] + ram_generations: OnceLock>, +} +impl Ip35Bus { + pub fn new(phys: Arc) -> Self { + phys.set_default(Arc::new(Unclaimed), "IP35 unclaimed cached address"); + Self { + phys, + views: std::array::from_fn(|_| SparseWindowMap::default()), + #[cfg(feature = "jitv2")] + ram_generations: OnceLock::new(), + } + } + /// Code pages use stable logical RAM generations across bank placement and + /// diagnostic remaps; non-RAM pages keep their own device generations. + #[cfg(feature = "jitv2")] + pub(crate) fn set_ram_generations(&self, memory: Arc) { + assert!(self.ram_generations.set(memory).is_ok(), "IP35 RAM generations already wired"); + } + /// Raw XKPHYS bits 58:57 select Hub-special, I/O, fetchop, or uncached RAM. + pub fn map(&mut self, raw_uc: u8, base: u64, size: u64, device: Arc) { + assert!(raw_uc < 4); + self.views[raw_uc as usize].map(base, size, device); + } + pub fn set_fallback(&mut self, raw_uc: u8, end: u64, device: Arc) { + self.map(raw_uc, 0, end, device); + } + fn target(&self, a: u64, n: u64) -> Option<(&SparseWindowMap, u64)> { + let pa = plain_address(a, n)?; + Some((&self.views[(a >> 57) as usize], pa)) + } + pub fn cached_view(&self, plain_memory: Arc) -> Arc { + Arc::new(CachedPhysical { + phys: self.phys.clone(), + plain_memory, + }) + } +} +fn plain_address(a: u64, n: u64) -> Option { + if a & !CPU_PHYS_MASK != 0 { + return None; + } + let pa = a & PA_MASK; + (pa.checked_add(n)? <= PA_MASK + 1).then_some(pa) +} +struct CachedPhysical { + phys: Arc, + plain_memory: Arc, +} +impl CachedPhysical { + fn memory_target(&self, a: u64, n: u64) -> Option<(&dyn BusDevice, u64)> { + self.target(a, n).map(|(physical, pa)| { + // MD owns every guest write to a possible fitted-RAM prefix, so + // all paths update its stable logical generation counters. PROM + // and other devices retain the cached Physical connection. + let memory: &dyn BusDevice = if pa / BANK_SPAN < 4 && pa % BANK_SPAN < FITTED_BANK { + self.plain_memory.as_ref() + } else { physical }; + (memory, pa) + }) + } + fn target(&self, a: u64, n: u64) -> Option<(&Physical, u64)> { + // UC selectors belong to the uncached SysAd connection. Reject them + // here rather than turning a malformed cached request into an alias. + if a & !PA_MASK != 0 || a & (n - 1) != 0 { + return None; + } + Some((self.phys.as_ref(), plain_address(a, n)?)) + } +} +macro_rules! processor_access { + ($r:ident,$w:ident,$t:ty,$out:ident,$n:expr,$writes:ident) => { + fn $r(&self, a: u64) -> $out { + self + .target(a, $n) + .map(|(v, pa)| v.$r(pa)) + .unwrap_or_else($out::err) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self + .$writes(a, $n) + .map(|(view, pa)| view.$w(pa, v)) + .unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for Ip35Bus { + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + if address & !PA_MASK == 0 { + // JIT page lookup passes plain physical code addresses, without + // a cache attribute. Raw UC0 therefore cannot select the MMIO map + // here: high cached RAM and the reset PROM use the cached plane. + if let Some(memory) = self.ram_generations.get() { + let generation = memory.gen_ptr(address); + if !generation.is_null() { return generation; } + } + return self.phys.gen_ptr(address); + } + self.target(address, 1).map(|(view, pa)| view.gen_ptr(pa)).unwrap_or(std::ptr::null()) + } + fn write64_masked(&self, a: u64, val: u64, mask: u64) -> u32 { + if a & 7 != 0 { + return BUS_ERR; + } + self.target(a, 8) + .map(|(view, pa)| view.write64_masked(pa, val, mask)) + .unwrap_or(BUS_ERR) + } + fn write_narrow_from(&self, a: u64, size: usize, gpr: u64) -> u32 { + self.target(a, size as u64) + .map(|(view, pa)| view.write_narrow_from(pa, size, gpr)) + .unwrap_or(BUS_ERR) + } + processor_access!(read8, write8, u8, BusRead8, 1, target); + processor_access!(read16, write16, u16, BusRead16, 2, target); + processor_access!(read32, write32, u32, BusRead32, 4, target); + processor_access!(read64, write64, u64, BusRead64, 8, target); +} +impl BusDevice for CachedPhysical { + fn write64_masked(&self, address: u64, value: u64, mask: u64) -> u32 { + self.memory_target(address, 8).map(|(view, pa)| view.write64_masked(pa, value, mask)) + .unwrap_or(BUS_ERR) + } + fn mem_ptr(&self, a: u64) -> Option<*const u64> { + self.target(a, 8).and_then(|(view, pa)| view.mem_ptr(pa)) + } + #[cfg(feature = "jitv2")] + fn gen_ptr(&self, a: u64) -> *const AtomicU64 { + self.target(a, 1).map(|(view, pa)| { + let generation = self.plain_memory.gen_ptr(pa); + if generation.is_null() { view.gen_ptr(pa) } else { generation } + }).unwrap_or(std::ptr::null()) + } + fn read_block(&self, a: u64, out: &mut [u64]) -> u32 { + self.target(a, 8) + .map(|(view, pa)| view.read_block(pa, out)).unwrap_or(BUS_ERR) + } + fn write_block(&self, a: u64, data: &[u64]) -> u32 { + self.memory_target(a, 8) + .map(|(view, pa)| view.write_block(pa, data)).unwrap_or(BUS_ERR) + } + processor_access!(read8, write8, u8, BusRead8, 1, memory_target); + processor_access!(read16, write16, u16, BusRead16, 2, memory_target); + processor_access!(read32, write32, u32, BusRead32, 4, memory_target); + processor_access!(read64, write64, u64, BusRead64, 8, memory_target); + fn dma_read64(&self, a: u64) -> BusRead64 { + plain_address(a, 8) + .map(|pa| self.plain_memory.dma_read64(pa)) + .unwrap_or_else(BusRead64::err) + } + fn dma_write64(&self, a: u64, v: u64) -> u32 { + plain_address(a, 8) + .map(|pa| self.plain_memory.dma_write64(pa, v)) + .unwrap_or(BUS_ERR) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ip35/tests.rs b/src/ip35/tests.rs new file mode 100644 index 000000000..32b246431 --- /dev/null +++ b/src/ip35/tests.rs @@ -0,0 +1,642 @@ +use super::*; +use crate::traits::{BusDevice, BUS_OK}; + +struct Word(u64); +impl BusDevice for Word { + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::ok(self.0) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum NarrowWriteCall { + Byte(u64, u8), + Half(u64, u16), + From(usize, u64, u64), +} +/// Records every narrow write it sees. With `sees_register` it overrides +/// `write_narrow_from` and keeps the whole source register, as Bedrock's ECC +/// view does; otherwise it takes the trait's default. +#[derive(Default)] +struct NarrowWriteSpy { + sees_register: bool, + calls: std::sync::Mutex>, +} +impl NarrowWriteSpy { + fn new(sees_register: bool) -> Arc { + Arc::new(Self { sees_register, ..Default::default() }) + } + fn calls(&self) -> Vec { + self.calls.lock().unwrap().clone() + } +} +impl BusDevice for NarrowWriteSpy { + fn write8(&self, address: u64, value: u8) -> u32 { + self.calls.lock().unwrap().push(NarrowWriteCall::Byte(address, value)); + BUS_OK + } + fn write16(&self, address: u64, value: u16) -> u32 { + self.calls.lock().unwrap().push(NarrowWriteCall::Half(address, value)); + BUS_OK + } + fn write_narrow_from(&self, address: u64, size: usize, gpr: u64) -> u32 { + if !self.sees_register { + return if size == 1 { self.write8(address, gpr as u8) } else { self.write16(address, gpr as u16) }; + } + self.calls.lock().unwrap().push(NarrowWriteCall::From(size, address, gpr)); + BUS_OK + } +} + +/// An uncached SB or SH reaches a device with the whole source register, at the +/// device-local address; only a device that overrides the call sees more than +/// the low bits, and a window outside every map is a bus error. +/// The Fuel's processor is the R14000 at revision 2.3 and 500 MHz, built as +/// the R12000 type with its PRId set over it; the Octane's own part is +/// revision 2.4. +#[test] +fn the_fuel_runs_the_r14000_at_revision_2_3() { + use crate::cpu::mips_cache_shadow::R12000ShadowCache; + use crate::cpu::mips_cache_v2::CpuModel as Model; + use crate::cpu::parts; + use crate::ip35_machine::CPUS; + assert_eq!(CPUS, &[&parts::R14000_500]); + let part = CPUS[0]; + assert_eq!((part.mhz, part.rev, part.l2_kb), (500, (2, 3), 2048)); + assert_eq!(part.prid(), 0x0f23); + assert_eq!(part.count_hz(), 250_000_000); + // `with_cpu!` builds the type `FuelCpu` names for this part. + let built = crate::with_cpu!(part, true, false, C => std::any::type_name::()); + assert_eq!(built, std::any::type_name::()); + // Typed R12000, it takes the R10000 family's behaviour: reserved + // secondary-tag bits read as zero, and a refused uncached store posts. + assert!(::R10K_FAMILY); + assert_ne!(::SECONDARY_TAG_MASK, u64::MAX); + assert!(::POSTS_UNCACHED_STORES); + assert!(::PARTIAL_LWR_KEEPS_UPPER); + // The executor carries the chip's PRId. + // The executor is large; the default test stack is not. + std::thread::Builder::new() + .stack_size(64 << 20) + .spawn(|| { + let cpu = crate::ip35_machine::build_cpu(Arc::new(Ip35Bus::new(Physical::new(Vec::new()))), crate::ip35_machine::CPUS[0]); + assert_eq!(cpu.core.cp0_prid, 0x0f23); + }) + .unwrap() + .join() + .expect("thread panicked"); +} + +#[test] +fn narrow_stores_carry_the_register_to_devices_that_ask_for_it() { + let mut bus = Ip35Bus::new(Physical::new(Vec::new())); + let plain = NarrowWriteSpy::new(false); + let ecc = NarrowWriteSpy::new(true); + bus.map(3, 0x1000, 0x1000, plain.clone()); + bus.map(3, 0x2000, 0x1000, ecc.clone()); + + let source = 0x1122_3344_5566_77a5u64; + let uc = 3u64 << 57; + assert_eq!(bus.write_narrow_from(uc | 0x1003, 1, source), BUS_OK); + assert_eq!(bus.write_narrow_from(uc | 0x1004, 2, source), BUS_OK); + assert_eq!( + plain.calls(), + vec![NarrowWriteCall::Byte(3, source as u8), NarrowWriteCall::Half(4, source as u16)], + "an ordinary window gets the architectural low bits", + ); + + assert_eq!(bus.write_narrow_from(uc | 0x2004, 2, source), BUS_OK); + assert_eq!(ecc.calls(), vec![NarrowWriteCall::From(2, 4, source)]); + + assert_eq!(bus.write_narrow_from(uc | 0x8000, 2, source), BUS_ERR, "no window there"); +} + +/// Equal physical offsets in different XKPHYS access spaces reach different devices. +#[test] +fn uc_spaces_keep_equal_offsets_distinct() { + let mut bus = Ip35Bus::new(Physical::new(Vec::new())); + for space in 0..4 { + bus.map( + space, + 0x0100_0000, + 0x1000, + Arc::new(Word(0x100 + space as u64)), + ); + } + for space in 0..4 { + let value = bus.read64(((space as u64) << 57) | 0x0100_0000); + assert_eq!(value.status, BUS_OK); + assert_eq!(value.data, 0x100 + space as u64); + } +} + +/// The processor must retain both XKPHYS access-space bits through translation. +#[test] +fn xkphys_uc_bits_survive_the_board_cpu_mask() { + // MipsExecutor and the TLB's inline arrays exceed the test worker's stack. + std::thread::Builder::new() + .stack_size(4 * 1024 * 1024) + .spawn(|| { + let bus = Arc::new(Ip35Bus::new(Physical::new(Vec::new()))); + let mut cpu = crate::ip35_machine::build_cpu(bus, crate::ip35_machine::CPUS[0]); + cpu.core.write_cp0( + 12, + (cpu.core.cp0_status | crate::cpu::mips_core::STATUS_KX) as u64, + ); + for raw_uc in 0..4u64 { + let va = 0x9000_0000_0100_0020 | (raw_uc << 57); + for _ in 0..2 { + let translated = cpu.debug_translate(va); + assert_eq!(translated.status, crate::cpu::mips_exec::TR_UNCACHED); + assert_eq!(translated.phys, (raw_uc << 57) | 0x0100_0020); + } + } + }) + .expect("spawn") + .join() + .expect("thread panicked"); +} + +/// RAM placed above the first GiB must remain shared by cached, uncached and DMA paths. +#[test] +fn cached_and_uncached_memory_keep_high_addresses() { + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + assert_eq!(board.md.md_bus().write64(8, 0x33), BUS_OK); + let at = (1 << 30) + 0x1000; + assert_eq!(board.md.memory_view().write64(at, 0x12345678), BUS_OK); + assert_eq!(board.cached_bus().read64(at).data, 0x12345678); + assert_eq!(board.bus.read64((3 << 57) | at).data, 0x12345678); + assert_eq!(board.bus.read64(at).status, BUS_ERR); + assert_eq!(board.md.dma_view().read64(at).data, 0x12345678); +} + +/// Memory diagnostics must notify both processor interfaces and refuse bad uncached data. +#[test] +fn memory_errors_reach_both_interfaces_and_uncached_reads_fail() { + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + let correctable = 1 << 61; + board.pi[0].write64(0xb0, correctable); + board.md.md_bus().write64(8, 0x33); + for directory in [false, true] { + if directory { + board.md.md_bus().write64(0x40000, 2); + board.md.directory_bus().write64(0, 1 << 16); + board.md.md_bus().write64(0x40000, 0); + } else { + board.md.ecc_bus().write8(7, 1); + } + assert_eq!(board.bus.read64(0).status, BUS_OK); + for pi in &board.pi { + assert_eq!(pi.read64(0xa0).data & correctable, correctable); + } + assert_eq!(board.irq.bits() & (1 << 11), 1 << 11); + for pi in &board.pi { + pi.write64(0x90, 64 + 61); + assert_eq!(pi.read64(0xa0).data & correctable, 0); + } + assert_eq!(board.irq.bits() & (1 << 11), 0); + } + + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + board.md.md_bus().write64(8, 0x33); + board.md.ecc_bus().write8(7, 3); + for selector in [0, 3] { + assert_eq!(board.bus.read64(selector << 57).status, BUS_ERR); + } + assert_eq!(board.cached_bus().read64(0).status, BUS_OK); + assert_eq!(board.md.dma_view().read64(0).status, BUS_OK); +} + +/// Firmware resets the PCI link before reinitializing the attached storage controller. +#[test] +fn fuel_link_reset_resets_the_attached_scsi_engine() { + use crate::dev::isp1040::{mbox, reg, CTRL_INT_ENABLE, CTRL_RISC_INT_ENABLE, HOST_CMD_SHIFT}; + use crate::pci::PciBusDevice; + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + let isp = &board.scsi; + isp.config().write(1, 6); + isp.config().write(5, 0x02000000); + isp.bar_write(1, reg::BUS_CTRL, u32::from(CTRL_INT_ENABLE | CTRL_RISC_INT_ENABLE), 2); + isp.bar_write(1, reg::MAILBOX_BASE, u32::from(mbox::NOP), 2); + isp.bar_write(1, reg::HOST_CMD, 0x5 << HOST_CMD_SHIFT, 2); + assert_eq!(isp.bar_read(1, reg::MAILBOX_BASE, 2), u32::from(mbox::COMPLETE)); + assert_ne!(isp.bar_read(1, reg::BUS_CTRL, 2), 0); + assert_eq!(board.xbridge.widget(0).unwrap().write32(0x2f4, 1), BUS_OK); + assert_eq!(isp.config().command(), 0); + assert_eq!(isp.config().bar_base(1), 0); + assert_eq!(isp.bar_read(1, reg::BUS_CTRL, 2), 0); + assert_eq!(isp.bar_read(1, reg::MAILBOX_BASE, 2), 0); + assert_eq!(isp.bar_read(1, reg::MAILBOX_BASE + 2, 2), 0x4953); +} + +/// The PROM reads the station address from the L1 and IRIX from the +/// Ethernet; both must report the configured one. +#[test] +fn the_configured_station_address_reaches_the_l1_and_the_ethernet() { + let mac = [0x08, 0x00, 0x69, 0x21, 0x43, 0x65]; + let board = crate::ip35_machine::FuelBoard::with_config(crate::ip35_machine::FuelBoardConfig { + payload: &[], clock: Arc::new(|| 0), flash_path: None, + station_mac: mac, + timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), + ps2_input: true, usb_input: true, + l1: Default::default(), + }).unwrap(); + assert_eq!(board.l1.station_mac(), mac); + assert_eq!(board.eth.mac(), mac); +} + +/// The L1's reported revision and brick serial keep their defaults unless +/// the board is built with replacements. +#[test] +fn the_l1_identity_overrides_reach_the_controller() { + use crate::ip35_machine::{FuelBoard, FuelBoardConfig, L1Identity}; + let build = |l1| FuelBoard::with_config(FuelBoardConfig { + payload: &[], clock: Arc::new(|| 0), flash_path: None, + station_mac: [8, 0, 0x69, 0x12, 0x34, 0x56], + timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), + ps2_input: true, usb_input: false, l1, + }).unwrap().l1.config(); + let stock = build(L1Identity::default()); + assert_eq!(stock.revision, Some([1, 48, 1])); + assert_eq!(stock.brick_serial.as_deref(), Some("EMUN00")); + let set = build(L1Identity { revision: Some([2, 3, 4]), brick_serial: Some("EMUN09".into()) }); + assert_eq!(set.revision, Some([2, 3, 4])); + assert_eq!(set.brick_serial.as_deref(), Some("EMUN09")); +} + +/// The Fuel's cache reports primary data tag stores to the memory controller: +/// the PROM's installs over its window start the private page, and its sweep +/// ends it. Other caches' tag stores and non-installs do nothing. +#[test] +fn the_prom_tag_stores_reach_the_memory_controller_through_the_cpu_cache() { + // An executor is large; a test thread's stack is not. + std::thread::Builder::new().stack_size(64 << 20).spawn(prom_tag_stores).unwrap().join().unwrap(); +} + +fn prom_tag_stores() { + use crate::cpu::mips_cache_v2::MipsCache; + use crate::cpu::mips_isa::{CACH_PD, CACH_PI, C_IST}; + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + let cpu = board.build_cpu(crate::ip35_machine::CPUS[0]); + let window = |at: u64| board.md.physical().read64(at).status == BUS_OK; + assert!(!window(0x10_0000)); + cpu.cache.cache_op(C_IST | CACH_PD, 0xa800_0000_0010_0000, 0xffff_ffcf); + cpu.cache.cache_op(C_IST | CACH_PI, 0xa800_0000_0010_0000, 0x100c0); + assert!(!window(0x10_0000), "a pattern and an instruction cache tag start nothing"); + cpu.cache.cache_op(C_IST | CACH_PD, 0xa800_0000_0010_0000, 0x100c0); + assert!(window(0x10_0000)); + cpu.cache.cache_op(C_IST | CACH_PD, 0xa800_0000_0010_0000, 0); + assert!(!window(0x10_0000)); +} + +fn fuel_dma_board() -> crate::ip35_machine::FuelBoard { + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + assert_eq!(board.md.md_bus().write64(8, 0x33), BUS_OK); + board.xbridge.head(15).write32(0x24, 15); + board.xbridge.set_wide_slot_mask(15, 0); + board +} + +fn fuel_dma_address(address: u64) -> u64 { + (10 << 60) | (1 << 55) | address +} + +/// Storage DMA buffers can begin unaligned and cross a physical page boundary. +#[test] +fn dma_preserves_byte_order_and_crosses_ragged_pages() { + use crate::pci::PciDmaStatus; + let board = fuel_dma_board(); + let dma = board.xbridge.dma(15, 0); + let memory = board.md.memory_view(); + for (address, length) in [(0x1030, 8), (0x1103, 27), (0x1ffb, 19)] { + let data: Vec = (0..length).map(|i| 0x31 + i as u8).collect(); + assert_eq!(dma.dma_write(fuel_dma_address(address), &data), PciDmaStatus::Ok); + for (i, &byte) in data.iter().enumerate() { + assert_eq!(memory.read8(address + i as u64).data, byte); + } + let mut back = vec![0xff; length]; + assert_eq!(dma.dma_read(fuel_dma_address(address), &mut back), PciDmaStatus::Ok); + assert_eq!(back, data); + } + // Without byte swapping enabled, the bridge reverses each 32-bit word. + assert_eq!(dma.dma_write((10 << 60) | 0x3000, &[0, 1, 2, 3, 4, 5, 6, 7]), PciDmaStatus::Ok); + assert_eq!(memory.read64(0x3000).data, 0x0302010007060504); +} + +/// A failed DMA read must clear only refused chunks and preserve completed data. +#[test] +fn dma_refusals_clear_failed_reads_without_losing_completed_chunks() { + use crate::pci::PciDmaStatus; + let board = fuel_dma_board(); + let dma = board.xbridge.dma(15, 0); + let memory = board.md.memory_view(); + // Bank zero has 256 MiB fitted. The second qword is outside its backing. + let last = 0x0fff_fff8; + assert_eq!(memory.write64(last, 0x0102030405060708), BUS_OK); + let mut bytes = [0xff; 16]; + assert_eq!(dma.dma_read(fuel_dma_address(last), &mut bytes), PciDmaStatus::MasterAbort); + assert_eq!(bytes, [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(dma.dma_write(fuel_dma_address(last), &[0x42; 16]), PciDmaStatus::MasterAbort); + assert_eq!(memory.read64(last).data, 0x4242424242424242); + board.io.disable_device(15, 0); + bytes.fill(0xff); + assert_eq!(dma.dma_read(fuel_dma_address(last), &mut bytes), PciDmaStatus::MasterAbort); + assert_eq!(bytes, [0; 16]); + assert_eq!(dma.dma_write(fuel_dma_address(last), &[0x99; 8]), PciDmaStatus::MasterAbort); + assert_eq!(memory.read64(last).data, 0x4242424242424242); +} + +/// A wide DMA leg must report an uncorrectable word once without publishing bad bytes. +#[test] +fn fuel_dma_checks_an_aligned_uncorrectable_word_once() { + use crate::dev::bedrock_md::MemoryError; + use crate::pci::PciDmaStatus; + use std::sync::atomic::{AtomicU64, Ordering}; + let board = fuel_dma_board(); + assert_eq!(board.md.memory_view().write64(0, 0), BUS_OK); + assert_eq!(board.md.ecc_bus().write8(7, 3), BUS_OK); + let count = Arc::new(AtomicU64::new(0)); + let seen = count.clone(); + let md = Arc::downgrade(&board.md); + board.md.on_error(Arc::new(move |kind| { + assert_eq!(kind, MemoryError::Uncorrectable); + assert_eq!(md.upgrade().unwrap().current_cell(), u64::MAX); + seen.fetch_add(1, Ordering::Relaxed); + })); + // The former callback read the same persistent UCE once for each byte. + for address in 0..8 { + assert_eq!(board.io.inbound_read(15, Some(0), address, 1).status, BUS_OK); + } + assert_eq!(count.swap(0, Ordering::Relaxed), 8); + assert_ne!(board.md.md_bus().read64(0x60018).data & (1 << 61), 0); + let mut bytes = [0xff; 8]; + assert_eq!(board.xbridge.dma(15, 0).dma_read(fuel_dma_address(0), &mut bytes), PciDmaStatus::Ok); + assert_eq!(bytes, [0; 8]); + assert_eq!(count.load(Ordering::Relaxed), 1); + let record = board.md.md_bus().read64(0x60010).data; + assert_ne!(record & (1 << 60), 0); + assert_eq!(record & (1 << 61), 0); + assert_eq!(board.md.uncached_view().read64(0).status, BUS_ERR); +} + +/// Inbound Hub writes must preserve access width and the requesting device identity. +#[test] +fn dma_hub_access_preserves_width_and_source_permissions() { + use crate::pci::PciDmaStatus; + let board = fuel_dma_board(); + let dma = board.xbridge.dma(15, 0); + let hub = (1 << 47) | 0x1800000; + for (interface, offset) in [(0, 0), (1, 0x200000)] { + let address = fuel_dma_address(hub + offset + 0x90); + assert_eq!(dma.dma_write(address, &0x10cu64.to_be_bytes()), PciDmaStatus::Ok); + assert_ne!(board.pi[interface].read64(0x98).data & (1 << 12), 0); + for width in [1, 2, 4] { + assert_eq!(dma.dma_write(address, &[0; 8][..width]), PciDmaStatus::MasterAbort); + assert_ne!(board.pi[interface].read64(0x98).data & (1 << 12), 0); + } + assert_eq!(dma.dma_write(address, &12u64.to_be_bytes()), PciDmaStatus::Ok); + assert_eq!(board.pi[interface].read64(0x98).data & (1 << 12), 0); + } + let address = fuel_dma_address(hub + 0x90); + // I/O modify must retain PI region protection and requesting-slot identity. + assert_eq!(board.pi[0].write64(0x10, 0), BUS_OK); + assert_eq!(dma.dma_write(address, &0x10cu64.to_be_bytes()), PciDmaStatus::MasterAbort); + assert_eq!(board.pi[0].read64(0x98).data & (1 << 12), 0); + assert_eq!(board.pi[0].write64(0x10, 1), BUS_OK); + board.io.disable_device(15, 0); + assert_eq!(dma.dma_write(address, &0x10cu64.to_be_bytes()), PciDmaStatus::MasterAbort); + assert_eq!(board.pi[0].read64(0x98).data & (1 << 12), 0); + assert_eq!(board.xbridge.dma(15, 1).dma_write(address, &0x10cu64.to_be_bytes()), PciDmaStatus::Ok); + assert_ne!(board.pi[0].read64(0x98).data & (1 << 12), 0); + let mut bytes = [0xff; 8]; + assert_eq!(board.xbridge.dma(15, 1).dma_read( + fuel_dma_address(hub + 0x40), &mut bytes, + ), PciDmaStatus::MasterAbort); + assert_eq!(bytes, [0; 8]); + assert_eq!(board.bus.read64((1 << 57) | 0x1800040).status, BUS_OK); +} + +/// Rounded MMIO pages must not expose padding or bypass cell permissions. +#[test] +fn exact_windows_reject_padding_and_revoked_permissions() { + use std::sync::atomic::{AtomicBool, Ordering}; + struct Offset; + impl BusDevice for Offset { + fn read64(&self, offset: u64) -> BusRead64 { BusRead64::ok(offset) } + fn write64(&self, _offset: u64, _value: u64) -> u32 { BUS_OK } + } + let permitted = Arc::new(AtomicBool::new(true)); + let permission = permitted.clone(); + let window = Window::new(0x1000, 0x88, Arc::new(Offset)) + .permitted(Arc::new(move || permission.load(Ordering::Acquire))); + assert_eq!(window.read64(0x1080).data, 0x80); + assert_eq!(window.read64(0x1088).status, BUS_ERR); + assert_eq!(window.read64(0x1084).status, BUS_ERR); + assert_eq!(window.read64(0x0ff8).status, BUS_ERR); + assert_eq!(window.read64(u64::MAX).status, BUS_ERR); + permitted.store(false, Ordering::Release); + assert_eq!(window.read64(0x1000).status, BUS_ERR); + assert_eq!(window.write64(0x1000, 1), BUS_ERR); + permitted.store(true, Ordering::Release); + assert_eq!(window.write64(0x1000, 1), BUS_OK); +} + +/// Board default setup must preserve the direct RAM path and its shared DMA contents. +#[test] +fn direct_cached_banks_remain_visible_after_board_defaults_are_installed() { + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + assert_eq!(board.md.md_bus().write64(8, 0x33), BUS_OK); + let cached = board.cached_bus(); + assert!(cached.mem_ptr(0x2000).is_some()); + assert_eq!(cached.write64(0x2000, 0x123456789abcdef0), BUS_OK); + assert_eq!(board.md.dma_view().read64(0x2000).data, 0x123456789abcdef0); + assert!(cached.mem_ptr(3 << 57).is_none(), "UC selectors cannot alias cached RAM"); +} + +/// A device overlay replaces its pages, including padding, while its neighbors +/// keep their original address translation. +#[test] +fn sparse_overlays_keep_neighbors_and_refuse_the_new_windows_padding() { + let mut map = SparseWindowMap::default(); + map.map(0x1000, 0x4000, Arc::new(Word(1))); + map.map(0x2040, 0x80, Arc::new(Word(2))); + assert_eq!(map.read64(0x1000).data, 1); + assert_eq!(map.read64(0x2040).data, 2); + assert_eq!(map.read64(0x20b8).data, 2); + assert_eq!(map.read64(0x20bc).status, BUS_ERR); + assert_eq!(map.read64(0x2000).status, BUS_ERR); + assert_eq!(map.read64(0x20c0).status, BUS_ERR); + assert_eq!(map.read64(0x3000).data, 1); + assert_eq!(map.read64(0x5000).status, BUS_ERR); + map.map(0x1000, 0x4000, Arc::new(Word(3))); + assert_eq!(map.ranges.len(), 1); + assert_eq!(map.read64(0x2040).data, 3); +} + +/// Sparse MMIO holes remain bus errors and a revoked cell permission does not +/// fall through to an older mapping beneath the protected window. +#[test] +fn sparse_holes_and_revoked_permissions_never_expose_older_devices() { + use std::sync::atomic::{AtomicBool, Ordering}; + let permission = Arc::new(AtomicBool::new(true)); + let allowed = permission.clone(); + let mut map = SparseWindowMap::default(); + map.map(0, 0x3000, Arc::new(Word(1))); + map.map(0x1000, 8, Arc::new(Window::new(0, 8, Arc::new(Word(2))) + .permitted(Arc::new(move || allowed.load(Ordering::Acquire))))); + assert_eq!(map.read64(0x1000).data, 2); + permission.store(false, Ordering::Release); + assert_eq!(map.read64(0x1000).status, BUS_ERR); + assert_eq!(map.read64(0x1008).status, BUS_ERR); + assert_eq!(map.read64(0x2000).data, 1); + assert_eq!(map.read64(0x3000).status, BUS_ERR); +} + +/// Code-page lookup receives plain physical addresses, while tagged uncached +/// requests retain their view. Cached stores must also update the MD view. +#[cfg(feature = "jitv2")] +#[test] +fn code_generations_use_the_cached_plane_and_stable_ram_endpoint() { + struct Generations { + ram_only: bool, + generation: AtomicU64, + writes: std::sync::Mutex>, + } + impl Generations { + fn new(ram_only: bool) -> Arc { + Arc::new(Self { ram_only, generation: AtomicU64::new(0), writes: Default::default() }) + } + } + impl BusDevice for Generations { + fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + if !self.ram_only || address < 0x1000 { &self.generation } else { std::ptr::null() } + } + fn write64(&self, address: u64, value: u64) -> u32 { + self.writes.lock().unwrap().push((address, value)); + BUS_OK + } + fn write64_masked(&self, address: u64, value: u64, mask: u64) -> u32 { + self.write64(address, value & mask) + } + fn write_block(&self, address: u64, values: &[u64]) -> u32 { + for (index, value) in values.iter().enumerate() { + self.write64(address + index as u64 * 8, *value); + } + BUS_OK + } + } + let physical = Physical::new(Vec::new()); + let mut bus = Ip35Bus::new(physical.clone()); + let raw_ram = Generations::new(false); + let stable_ram = Generations::new(true); + let rom = Generations::new(false); + map_device(&physical, 0, 0x1000, raw_ram.clone(), "raw RAM generation"); + map_device(&physical, 0x1fc00000, 0x1000, rom.clone(), "ROM generation"); + bus.set_ram_generations(stable_ram.clone()); + bus.map(3, 0, 0x1000, stable_ram.clone()); + bus.map(3, 0x3000, 8, stable_ram.clone()); + let cached = bus.cached_view(stable_ram.clone()); + assert_eq!(bus.gen_ptr(0x40), &stable_ram.generation as *const AtomicU64); + assert_eq!(cached.gen_ptr(0x40), &stable_ram.generation as *const AtomicU64); + assert_eq!(bus.gen_ptr(0x1fc00000), &rom.generation as *const AtomicU64); + assert_eq!(cached.gen_ptr(0x1fc00000), &rom.generation as *const AtomicU64); + assert_eq!(bus.gen_ptr((3 << 57) | 0x40), &stable_ram.generation as *const AtomicU64); + assert_eq!(bus.gen_ptr((3 << 57) | 0x3000), &stable_ram.generation as *const AtomicU64); + assert!(bus.gen_ptr((3 << 57) | 0x3008).is_null()); + assert!(bus.gen_ptr((1 << 57) | 0x1fc00000).is_null()); + assert!(bus.gen_ptr(1 << 50).is_null()); + assert_eq!(cached.write64(0x40, 0x1234), BUS_OK); + assert_eq!(cached.write_block(0x48, &[0x5678]), BUS_OK); + assert_eq!(cached.write64_masked(0x50, 0xaabb, 0xff), BUS_OK); + assert_eq!(*stable_ram.writes.lock().unwrap(), [(0x40, 0x1234), (0x48, 0x5678), (0x50, 0xbb)]); + assert_eq!(cached.write64(0x1fc00000, 0x4321), BUS_OK); + assert_eq!(*rom.writes.lock().unwrap(), [(0, 0x4321)], "the ROM window translates to a device-local offset"); + assert!(raw_ram.writes.lock().unwrap().is_empty()); +} + +/// Unmodeled Hub heads retain the PROM compatibility readback without +/// absorbing the adjacent, independently implemented L1 UART window. +#[test] +fn compatibility_hub_heads_do_not_hide_the_l1_uart_window() { + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + assert_ne!(board.md.current_cell() & 1, 0); + for base in [0x1000_0000, 0x3000_0000] { + for offset in [0, 8, 0x40, 0x78] { + let address = base + offset; + assert_eq!(board.bus.write8(address, 0xa5), BUS_OK); + assert_eq!(board.bus.write16(address, 0xa5a5), BUS_OK); + assert_eq!(board.bus.write32(address, 0xa5a5a5a5), BUS_OK); + assert_eq!(board.bus.write64(address, 0x2000), BUS_OK); + let value = board.bus.read8(address); + assert_eq!(value.status, BUS_OK); + assert_eq!(value.data, 0); + let value = board.bus.read16(address); + assert_eq!(value.status, BUS_OK); + assert_eq!(value.data, 0); + let value = board.bus.read32(address); + assert_eq!(value.status, BUS_OK); + assert_eq!(value.data, 0); + let value = board.bus.read64(address); + assert_eq!(value.status, BUS_OK); + assert_eq!(value.data, 0); + } + assert_eq!(board.bus.read64(base + 0x80).status, BUS_OK); + } +} + +/// A busy host port or unusable transcript path must not prevent a serial +/// machine from starting, including when any of its three ports serves CI. +#[test] +fn unavailable_serial_ports_and_logs_do_not_abort_the_machine() { + use crate::config::{FuelConsole, MachineConfig}; + let listeners: Vec<_> = (0..3).map(|_| std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap()).collect(); + let ports: Vec<_> = listeners.iter().map(|socket| socket.local_addr().unwrap().port()).collect(); + let board = crate::ip35_machine::FuelBoard::new(&[], Arc::new(|| 0)).unwrap(); + let mut config = MachineConfig::default(); + config.fuel.l1_port = Some(ports[0]); + config.serial_port_a = Some(ports[1]); + config.serial_port_b = Some(ports[2]); + config.ci = true; + let missing = std::env::temp_dir().join(format!("iris-fuel-missing-log-{}", std::process::id())).join("absent").join("serial.txt"); + assert!(!missing.parent().unwrap().exists(), "the log parent must be absent"); + config.serial_log = Some(missing.to_string_lossy().into_owned()); + config.fuel.l1_log = config.serial_log.clone(); + for (console, selection) in [FuelConsole::L1, FuelConsole::Ioc3A, FuelConsole::Ioc3B].into_iter().enumerate() { + config.fuel.console = selection; + crate::ip35_machine::FuelMachine::connect_serial_for_test(&config, &board, console).unwrap(); + } + config.ci = false; + crate::ip35_machine::FuelMachine::connect_serial_for_test(&config, &board, 0).unwrap(); +} + +/// The Fuel's OHCI root ports each carry a low-speed device, and host +/// input reaches them through the IOC3's PS/2 controller object; the USB +/// pair is fitted only when asked for. +#[test] +fn the_fuel_plugs_a_usb_keyboard_and_mouse_only_when_configured_on() { + use crate::pci::PciBusDevice; + let ports = |usb_input| { + let board = crate::ip35_machine::FuelBoard::with_config(crate::ip35_machine::FuelBoardConfig { + payload: &[], clock: Arc::new(|| 0), flash_path: None, + station_mac: [8, 0, 0x69, 0x12, 0x34, 0x56], + timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), + ps2_input: true, usb_input, + l1: Default::default(), + }).unwrap(); + // Power each port: one with a device shows it connected. + board.usb.bar_write(0, 0x54, 0x100, 4); + board.usb.bar_write(0, 0x58, 0x100, 4); + let status = [0x54, 0x58].map(|at| board.usb.bar_read(0, at, 4)); + (board, status) + }; + let (board, status) = ports(true); + for s in status { + assert_eq!(s & 1, 1, "device connected"); + assert_eq!(s & 0x200, 0x200, "low speed"); + } + // The mouse the USB side fits makes the shared controller report a mouse + // only once the guest has configured it. + assert!(!board.ioc3.ps2().mouse_enabled()); + let (_, status) = ports(false); + assert_eq!(status.map(|s| s & 1), [0, 0], "nothing plugged in"); +} diff --git a/src/ip35_machine.rs b/src/ip35_machine.rs new file mode 100644 index 000000000..66a46ca34 --- /dev/null +++ b/src/ip35_machine.rs @@ -0,0 +1,1107 @@ +//! The Fuel (IP34 board, IP35 software platform): an R14000 on Bedrock, +//! XBridge and the base I/O devices, with Iris's CPU thread, monitor and serial console. +//! +//! Selected with `[machine] profile = "fuel_ip34"`. Shared keys select the +//! PROM, NVRAM, memory and SCSI devices; `[fuel]` selects the serial console +//! and writable flash. `ip35::Ip35Bus` maps the processor views over the +//! same Physical memory banks. Graphics is not attached, and the IOC3 has +//! no keyboard or mouse ports. +//! +//! During PROM cache-as-memory bootstrap, cached startup bytes live in a +//! Fuel cache adapter until a successful cached fetch leaves the PROM homes. +//! This retirement rule is an approximation, not an established hardware event. +//! +//! # What is approximated here +//! +//! The two Hub-special head windows return zero on reads and acknowledge +//! writes without side effects. PROM 6.210 writes the 64-bit value 0x2000 +//! at relative offsets 0 and 0x40 before DRAM discovery. Their hardware +//! meaning is unknown; Bedrock logging records accesses to these windows. +//! +//! Board composition ported from hw/mips/sgi/sn1-node.c +//! in jamesbraid/qemu-sgi-origami. + +use crate::config::{scsi_label, FuelConsole, MachineConfig}; +use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; +use crate::cpu::mips_tlb::MipsTlb; +use crate::dev::bedrock_flash::{Flash, PART_SIZE}; +use crate::dev::bedrock_io::{BedrockIo, SIZE as IO_SIZE}; +use crate::dev::bedrock_lb::{BedrockLb, SIZE as LB_SIZE}; +use crate::dev::bedrock_md::{BedrockMd, MemoryError}; +use crate::dev::bedrock_ni::{BedrockNi, SIZE as NI_SIZE}; +use crate::dev::bedrock_pi::{reg as pi_reg, BedrockPi, BedrockPiConfig, SIZE as PI_SIZE}; +use crate::dev::bedrock_xb::{BedrockXb, SIZE as XB_SIZE}; +use crate::dev::heart::{host_clock, Clock}; +use crate::dev::ioc3::{Ioc3, Ioc3Config}; +use crate::dev::ioc3_eth::Ioc3Eth; +use crate::dev::isp12160::Isp12160; +use crate::dev::l1::{L1, L1Config, L1Event}; +use crate::dev::ohci::Ohci; +use crate::dev::usb_hid::{UsbKeyboard, UsbMouse}; +use crate::dev::prom_image; +use crate::dev::ps2::Ps2Controller; +use crate::dev::serial_host::{null_serial_backend, open_log, tcp_serial_backend, CiSerialBackend, LogMode, SerialBackend, TeeBackend}; +use crate::dev::timekeeper::Timekeeper; +use crate::dev::xbridge::XBridge; +use crate::devlog::{init_devlog, LogModule}; +use crate::gdb_stub::CpuDebug; +use crate::gfx_display::GfxDisplay; +use crate::hptimer::TimerManager; +use crate::cpu::mips_cache_shadow::R12000ShadowCache; +use crate::cpu::parts::{self, Part}; +use crate::cpu::mips_cache_v2::MipsCache; +use crate::ip35::{map_device, Ip35Bus, SparseWindowMap, Window, CPU_PHYS_MASK}; +use crate::irq::{InterruptController, IrqLine}; +use crate::machine_state::GuestState; +use crate::monitor::{Monitor, DEFAULT_PORT as DEFAULT_MONITOR_PORT}; +use crate::net::{GatewayConfig, Link, NatControl}; +use crate::perf_monitor::PerfMonitor; +use crate::ppmem::devmap::SLOT_SHIFT; +use crate::scsi::{attach_from_config, ScsiBus}; +use crate::traits::{BusDevice, BusRead8, BusRead16, BusRead32, BusRead64, Device, MachineEvent, Resettable, Signal, BUS_ERR, BUS_OK}; +use crate::workstation::{register_system_controller, DiscChanger, MachineRef, MachineSlot, Workstation}; +use parking_lot::Mutex; +use std::net::{Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{channel, Receiver}; +use std::sync::{atomic::{AtomicBool, AtomicU64, Ordering}, Arc}; + +pub struct FuelMachine { + board: Arc, + cpu: Arc, + timers: Arc, + monitor: Arc, + monitor_port: u16, + ci_serial: Option>, + console: usize, + ci: bool, + scratch: Option, + timekeeper: Arc, + nvram: String, + frames: Option>, + nat: Option<( + Arc, + (Ipv4Addr, Ipv4Addr, Ipv4Addr), + )>, + events: Option>, + slot: Option, + lifecycle: GuestState, +} + +impl FuelMachine { + pub fn new(cfg: &MachineConfig) -> Result { + cfg.validate()?; + let prom = prom_image::load(Path::new(&cfg.prom), 8 * 1024 * 1024) + .map_err(|e| format!("Fuel {e}"))?; + let timekeeper = Arc::new(Timekeeper::fuel_with_nvram_file( + Path::new(&cfg.nvram), + )?); + let board = Arc::new(FuelBoard::with_config(FuelBoardConfig { + payload: &prom, + clock: host_clock(), + flash_path: cfg.fuel.flash.as_deref().map(Path::new), + station_mac: cfg.network().mac, + timekeeper: timekeeper.clone(), + ps2_input: cfg.fuel.ps2_input, + usb_input: cfg.fuel.usb_input, + l1: L1Identity { + revision: cfg.fuel.l1_revision.as_deref().map(crate::dev::l1::parse_revision).transpose()?, + brick_serial: cfg.fuel.brick_serial.clone(), + }, + })?); + let timers = Arc::new(TimerManager::new()); + let part = cfg.cpu(); + let mut exec = board.build_cpu(part); + exec.core.set_timer_manager(timers.clone()); + exec.core.set_count_hz( + cfg.clock + .fixed_mhz + .map_or(part.count_hz(), |mhz| (mhz * 1_000_000.0) as u64) + .max(1), + ); + exec.core.count_write_samples = true; + let poll_board = board.clone(); + exec.batch_hook = Some(Box::new(move |_| poll_board.poll())); + let cpu: Arc = Arc::new(MipsCpu::<_, R12000ShadowCache>::new(*exec)); + if let Some(space) = board.md.physical().ppmem_space() { + use crate::ppmem::MappedMemory; + unsafe { + space.set_bitmap_sink(cpu.ppmem_bitmap_ptr()); + cpu.set_tcache_window(space.window_base()); + let bitmap = cpu.tcache_bitmap_ptr(); + if !bitmap.is_null() { space.set_bitmap_sink2(bitmap); } + #[cfg(feature = "jitv2")] + cpu.set_tcache_gen_window(board.md.generation_base()); + } + board.md.physical().resync_ppmem_bitmap(); + board.md.rebuild_memory(); + } + // After the windows above: each compile worker takes the cache's + // geometry and the CPU model's policies once, as it starts, and + // without the windows the geometry has no inline load or store path. + // Workers read plain physical code addresses, which the cached plane + // serves; the uncached views would decode them as Hub registers. + #[cfg(feature = "jitv2")] + { + let cpu_device: Arc = cpu.clone().as_device(); + let jit = cpu.jitv2(); + let mut g = jit.lock(); + g.compile_queue.set_thread_count(cfg.jitv2.threads.max(1)); + g.compile_queue.set_cpu(Arc::downgrade(&cpu_device)); + g.compile_queue.set_owner(Arc::downgrade(&jit)); + // Not under lockstep, as on the other machines: lockstep compiles + // inline on the CPU thread, and a running pool both owns the + // Codegen inline needs and deadlocks the CPU thread's flush. + #[cfg(not(feature = "jitv2_lockstep"))] + { + let stats = g.stats.clone(); + g.compile_queue.start(board.cached_bus(), stats); + } + // The compile pool keeps only a weak handle on the CPU. + std::mem::forget(cpu_device); + } + board.pi[0].set_interrupt_controller(Arc::new(CpuIrq(Arc::downgrade(&cpu)))); + let (tx, rx) = channel(); + let reset_tx = tx.clone(); + board.lb.connect_soft_reset(Arc::new(move || { + let _ = reset_tx.send(MachineEvent::HardReset); + })); + let reset_tx = tx.clone(); + board.ni.connect_local_reset(Arc::new(move || { + let _ = reset_tx.send(MachineEvent::HardReset); + })); + let weak_cpu = Arc::downgrade(&cpu); + board.l1.set_event_handler(Arc::new(move |event| { + match event { + L1Event::Power(false) | L1Event::Shutdown => { + let _ = tx.send(MachineEvent::PowerOff); + } + L1Event::Power(true) | L1Event::Reset => { + let _ = tx.send(MachineEvent::HardReset); + } + L1Event::SoftReset => { + if let Some(cpu) = weak_cpu.upgrade() { + cpu.as_device().signal(Signal::Reset(true)); + } + } + L1Event::Nmi => dlog!(LogModule::L1, "Fuel: NMI entry is not implemented by the CPU"), + } + })); + let scratch = Self::attach_scsi(cfg, &board)?; + let console = match cfg.fuel.console { + FuelConsole::L1 => 0, + FuelConsole::Ioc3A => 1, + FuelConsole::Ioc3B => 2, + }; + let ci_serial = cfg.ci.then(|| board.serial[console].clone()); + Self::connect_serial(cfg, &board, console)?; + let monitor = Monitor::new(); + monitor.register_device(init_devlog()); + monitor.register_device(Arc::new(crate::cpu::cpu_monitor::CpuMonitor::new(vec![cpu.clone()]))); + monitor.register_device(PerfMonitor::new( + cpu.running_flag(), cpu.cycles_ptr(), cpu.fasttick_count(), None, None, + )); + monitor.register_device(board.md.physical().clone()); + monitor.register_device(board.l1.clone()); + monitor.register_device(board.scsi.clone()); + // `ps2 type` and friends: the keyboard and mouse from the monitor. + monitor.register_device(board.ioc3.ps2()); + cpu.register_locks(); + board.scsi.register_locks(); + board.ioc3.register_locks(); + board.xbridge.register_locks(); + board.flash.register_locks(); + board.md.register_locks(); + let (mut nat, mut frames) = (None, None); + let net = GatewayConfig::from_network(&cfg.network()); + if let Some(link) = Link::with_gateway(net.clone())? { + frames = Some(link.frames()); + nat = Some((link.nat(), (net.client_ip, net.gateway_ip, net.netmask))); + board.eth.attach(link); + } + eprintln!( + "iris: Fuel IP34, {}, 512 MiB, serial console {:?}", + cfg.cpu().label(), cfg.fuel.console + ); + Ok(Self { + board, + cpu, + timers, + monitor: Arc::new(monitor), + monitor_port: cfg.monitor_port.unwrap_or(DEFAULT_MONITOR_PORT), + ci_serial, + console, + ci: cfg.ci, + scratch, + timekeeper, + nvram: cfg.nvram.clone(), + nat, + frames, + events: Some(rx), + slot: None, + lifecycle: GuestState::Idle, + }) + } + + fn attach_scsi(cfg: &MachineConfig, board: &FuelBoard) -> Result, String> { + let attached = attach_from_config(cfg, Some("fuel"), &|channel| { + board.scsi.bus_for_channel(channel) + .ok_or_else(|| format!("controller {channel} (a Fuel has channels 0 and 1)")) + })?; + if let Some(device) = attached.daynaports.first() { + return Err(format!("{}: kind = \"daynaport\" is not available on the Fuel", + scsi_label(device.controller, device.id))); + } + Ok(attached.scratch) + } + + fn connect_serial( + cfg: &MachineConfig, + board: &FuelBoard, + console: usize, + ) -> Result<(), String> { + let [a, b] = cfg.serial_ports(); + let ports = [cfg.fuel.l1_port.unwrap_or(8882), a, b]; + if ports[0] == ports[1] || ports[0] == ports[2] || ports[1] == ports[2] { + return Err("Fuel L1 and IOC3 serial ports must be distinct".into()); + } + let labels = ["L1", "IOC3 A", "IOC3 B"]; + let settings = ["fuel.l1_port", "serial_port_a", "serial_port_b"]; + let mut log_config = cfg.clone(); + for (index, port) in ports.into_iter().enumerate() { + log_config.serial_log = if index == console { + cfg.serial_log.clone() + } else if index == 0 { + cfg.fuel.l1_log.clone() + } else { + None + }; + let tag = format!("Fuel {}", labels[index]); + let log = open_log(&log_config, &tag, LogMode::Fresh); + let backend: Arc = if cfg.ci && index == console { + if let Some(log) = log { + board.serial[index].set_log(log); + } + board.serial[index].clone() + } else { + let mut backend = null_serial_backend(); + match tcp_serial_backend(port) { + Some((socket, actual_port)) => { + eprintln!("iris: {tag} on 127.0.0.1:{actual_port}"); + backend = socket; + } + None => eprintln!( + "iris: {tag}: port {port} unavailable; is another iris using it? ({})", + settings[index] + ), + } + match log { + Some(log) => Arc::new(TeeBackend::new(backend, log)), + None => backend, + } + }; + if index == 0 { + board.l1.set_serial_backend(backend); + } else { + board.ioc3.set_serial_backend(index - 1, backend); + } + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn connect_serial_for_test(cfg: &MachineConfig, board: &FuelBoard, console: usize) -> Result<(), String> { + Self::connect_serial(cfg, board, console) + } + +} + +struct CpuIrq(std::sync::Weak); +impl InterruptController for CpuIrq { + fn set_line(&self, line: u32, level: bool) { + if line < 5 { + if let Some(cpu) = self.0.upgrade() { + cpu.as_device().signal(Signal::Interrupt(line + 2, level)); + } + } + } +} + +impl Workstation for FuelMachine { + fn start(&mut self) { + self.lifecycle = GuestState::Starting; + self.monitor + .clone() + .start_server(format!("127.0.0.1:{}", self.monitor_port)); + self.restart_peripherals(); + if !self.ci { + self.cpu_start(); + } + } + fn stop(&mut self) { + self.cpu.clone().as_device().stop(); + self.board.stop(); + if let Err(e) = self.timekeeper.flush_nvram() { + dlog!(LogModule::Bedrock, "Fuel NVRAM: {e}"); + } + if self.lifecycle != GuestState::Idle { + self.lifecycle = GuestState::Halted; + } + } + fn reset(&mut self) { + self.stop(); + self.cpu.power_on(); + for pi in &self.board.pi { + pi.power_on(); + } + self.board.md.power_on(); + self.board.lb.power_on(); + self.board.io.power_on(); + self.board.ni.power_on(); + self.board.xb.power_on(); + self.board.l1.power_on(); + self.board.flash.power_on(); + self.board.xbridge.power_on(); + self.board.ioc3.power_on(); + self.board.eth.power_on(); + self.board.scsi.power_on(); + self.board.usb.power_on(); + self.timekeeper.power_on(); + self.restart_peripherals(); + } + fn restart_peripherals(&mut self) { + self.board.start(); + } + fn cpu_start(&self) { + self.cpu.clone().as_device().start(); + } + fn cpu_is_running(&self) -> bool { + self.cpu.clone().as_device().is_running() + } + fn machine_slot(&mut self) -> MachineSlot { + let ptr = self as *mut Self as *mut dyn Workstation; + self.slot + .get_or_insert_with(|| Arc::new(Mutex::new(MachineRef(ptr)))) + .clone() + } + fn register_system_controller(&mut self) { + let slot = self.machine_slot(); + let events = self + .events + .take() + .expect("Fuel system controller registered twice"); + register_system_controller(&self.monitor, slot, Some(events)); + } + fn monitor_addr(&self) -> Option { + self.monitor.bound_addr() + } + fn lifecycle(&self) -> GuestState { + self.lifecycle + } + fn set_lifecycle(&mut self, state: GuestState) { + self.lifecycle = state; + } + fn get_display(&self) -> Option> { + None + } + fn register_monitor_device(&self, device: Arc) { + self.monitor.register_device(device); + } + fn get_ps2(&self) -> Arc { + self.board.ioc3.ps2() + } + fn get_timer_manager(&self) -> Arc { + self.timers.clone() + } + fn get_cpu_debug(&self) -> Arc { + self.cpu.clone().debug_adapter() + } + fn fasttick_count(&self) -> Arc { + self.cpu.fasttick_count() + } + fn get_ci_serial(&self) -> Option> { + self.ci_serial.clone() + } + fn inject_serial_console(&self, bytes: &[u8]) { + self.board.serial[self.console].push_host(bytes); + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn save_nvram(&self, path: Option<&str>) -> Result { + if path.is_some_and(|p| p != self.nvram) { + return Err("Fuel NVRAM save uses the configured path".into()); + } + self.timekeeper.flush_nvram()?; + Ok(self.nvram.clone()) + } + fn scratch_path(&self) -> Option<&Path> { + self.scratch.as_deref() + } + fn disc_changer(&self) -> Option> { + Some(self.board.scsi.clone() as Arc) + } + fn pending_chd_sync_count(&self) -> usize { + (0..2).map(|channel| self.board.scsi.bus_for_channel(channel).unwrap().pending_chd_sync_count()).sum() + } + fn flush_disks(&self) -> std::io::Result<()> { + self.board.flush_disks() + } + fn sync_chd_disks( + &self, + only: Option, + progress: &mut dyn FnMut(usize, usize, f32), + cancel: &dyn Fn() -> bool, + ) -> std::io::Result { + let buses: Vec<_> = (0..2).map(|channel| self.board.scsi.bus_for_channel(channel).unwrap()).collect(); + ScsiBus::sync_chd_disks(&buses, only, progress, cancel) + } + fn net_frames(&self) -> Option> { self.frames.clone() } + fn nat_control(&self) -> Option> { + self.nat.as_ref().map(|(n, _)| n.clone()) + } + fn nat_expected(&self) -> Option<(Ipv4Addr, Ipv4Addr, Ipv4Addr)> { + self.nat.as_ref().map(|(_, a)| *a) + } +} + +impl Drop for FuelMachine { + fn drop(&mut self) { + if let Some(slot) = self.slot.take() { + slot.lock().0 = MachineRef::null(); + } + self.stop(); + self.monitor.shutdown(); + } +} + +/// The processors a Fuel can be built with, the default first: the R14000A +/// the Fuel ships with. +pub const CPUS: &[&Part] = &[&parts::R14000_500]; + +/// The cache type `with_cpu!` builds for every entry of [`CPUS`]; a test pins +/// it. The board's devices are not generic over the processor, so it names the +/// type once here. +pub(crate) type FuelCpu = MipsExecutor; + +// The 500 MHz / 2 MiB PROM record selects these boot-mode inputs. +const FUEL_CP0_CONFIG: u32 = 0x6c9a_a985; +const FUEL_BOARD_ID: u64 = 0x4004; + +/// The local node register blocks projected into the processor's I/O view. +pub mod reg { + pub const PI1_BASE: u64 = 0x20_0000; + pub const IO_BASE: u64 = 0x40_0000; + pub const LB_BASE: u64 = 0x60_0000; + pub const NI_BASE: u64 = 0x68_0000; + pub const XB_BASE: u64 = 0x70_0000; + pub const MD_BASE: u64 = 0x78_0000; + pub const MD_SIZE: u64 = 0x8_0000; +} + + +/// Build the Fuel's processor, `part` being the one the config resolved, and +/// set the PROM boot-mode inputs. +pub(crate) fn build_cpu(bus: Arc, part: &Part) -> Box { + let cfg = MipsCpuConfig::for_model::(Arc::new(crate::ip35::memory_map())) + .with_prid(part.prid()); + let mut cpu = Box::new(MipsExecutor::new( + bus as Arc, + MipsTlb::new(cfg.tlb_entries), + &cfg, + )); + cpu.phys_mask = CPU_PHYS_MASK; + // Fuel's 500 MHz/2 MiB flash record selects these boot-mode inputs. + // A mismatch enters the PROM's error routine before GP is initialized. + cpu.core.cp0_config = FUEL_CP0_CONFIG; + cpu +} + +/// Shared device composition for a single-processor Fuel. +/// Graphics is absent; the serial workstation shares these base I/O devices. +pub(crate) struct FuelBoard { + pub bus: Arc, + pub pi: [Arc; 2], + pub md: Arc, + pub l1: Arc, + pub flash: Arc, + pub lb: Arc, + pub io: Arc, + pub ni: Arc, + pub xb: Arc, + pub xbridge: Arc, + pub ioc3: Arc, + pub eth: Arc, + pub scsi: Arc, + pub usb: Arc, + pub serial: [Arc; 3], + #[cfg(test)] + pub irq: Arc, + clock: Clock, + last_clock: AtomicU64, + running: AtomicBool, +} + +pub(crate) struct FuelBoardConfig<'a> { + pub payload: &'a [u8], + pub clock: Clock, + pub flash_path: Option<&'a Path>, + pub station_mac: [u8; 6], + pub timekeeper: Arc, + /// The IOC3's PS/2 keyboard and mouse controller. + pub ps2_input: bool, + /// A USB keyboard and mouse on the OHCI root ports. + pub usb_input: bool, + /// Replacements for the L1's reported identity; unset keeps its defaults. + pub l1: L1Identity, +} + +/// What a Fuel config may replace in the L1's identity replies. +#[derive(Clone, Debug, Default)] +pub(crate) struct L1Identity { + pub revision: Option<[u32; 3]>, + pub brick_serial: Option, +} + +impl FuelBoard { + /// Flush the disks on both channels of the storage controller. + pub fn flush_disks(&self) -> std::io::Result<()> { + (0..2).try_for_each(|channel| self.scsi.bus_for_channel(channel).unwrap().flush()) + } + + #[cfg(test)] + pub fn new(payload: &[u8], clock: Clock) -> Result { + Self::with_config(FuelBoardConfig { + payload, clock, flash_path: None, + station_mac: L1Config::default().mac, + timekeeper: Arc::new(Timekeeper::fuel()?), + ps2_input: true, + usb_input: false, + l1: L1Identity::default(), + }) + } + + pub fn with_config(config: FuelBoardConfig<'_>) -> Result { + let FuelBoardConfig { payload, clock, flash_path, station_mac, timekeeper, ps2_input, usb_input, l1: l1_identity } = config; + let md = BedrockMd::with_clock(clock.clone()); + let phys = md.physical().clone(); + let pi: [Arc; 2] = std::array::from_fn(|interface| { + BedrockPi::new( + BedrockPiConfig { + interface: interface as u8, + request_region: 0, + }, + clock.clone(), + ) + }); + // Only CPU A is fitted on the single-processor board. + pi[0].write64(pi_reg::CPU_A_ENABLE, 1); + let mut l1_config = L1Config { mac: station_mac, ..Default::default() }; + if let Some(revision) = l1_identity.revision { + l1_config.revision = Some(revision); + } + if let Some(serial) = l1_identity.brick_serial { + l1_config.brick_serial = Some(serial); + } + let l1 = Arc::new(L1::new(l1_config)?); + let flash = Arc::new(match flash_path { + Some(path) => Flash::with_file(payload, path), + None => Flash::new(payload), + }.map_err(|e| e.to_string())?); + let lb = Arc::new(BedrockLb::new(clock.clone())); + let io = Arc::new(BedrockIo::new()); + let ni = Arc::new(BedrockNi::new()); + let xb = Arc::new(BedrockXb::new()); + let mut registers = SparseWindowMap::default(); + registers.map( + 0, + PI_SIZE, + pi[0].clone(), + ); + registers.map( + reg::PI1_BASE, + PI_SIZE, + pi[1].clone(), + ); + registers.map( + reg::IO_BASE, + IO_SIZE, + io.clone(), + ); + registers.map( + reg::LB_BASE, + LB_SIZE, + lb.clone(), + ); + registers.map( + reg::NI_BASE, + NI_SIZE, + ni.clone(), + ); + registers.map( + reg::XB_BASE, + XB_SIZE, + xb.clone(), + ); + registers.map( + reg::MD_BASE, + reg::MD_SIZE, + md.md_bus(), + ); + let registers = Arc::new(registers); + io.connect_hub(Arc::new(IoHub(pi.clone()))); + io.connect_memory(md.dma_view()); + let p = pi.clone(); + lb.connect_error(Arc::new(move |level| { + for pi in &p { + pi.set_lb_error(level); + } + })); + let p = pi.clone(); + ni.connect_error(Arc::new(move |level| { + for pi in &p { + pi.set_ni_error(level); + } + })); + let p = pi.clone(); + xb.connect_error(Arc::new(move |level| { + for pi in &p { + pi.set_xb_error(level); + } + })); + let io_node = Arc::downgrade(&io); + lb.connect_node_changed(Arc::new(move |node| { + if let Some(io) = io_node.upgrade() { + io.set_node_number(node); + } + })); + let p = pi.clone(); + md.on_error(Arc::new(move |kind| { + for pi in &p { + match kind { + MemoryError::Correctable => pi.set_pending(1, 61), + MemoryError::Uncorrectable => { + pi.set_memory_uncorrectable(); + } + } + } + })); + #[cfg(test)] + let irq = Arc::new(PendingIrq::default()); + #[cfg(test)] + pi[0].set_interrupt_controller(irq.clone()); + l1.set_irq(IrqLine::new(Arc::new(UartIrq(pi.clone())), 0)); + let serial = std::array::from_fn(|_| Arc::new(CiSerialBackend::new())); + l1.set_serial_backend(serial[0].clone()); + let xbridge = XBridge::new(md.dma_view()); + for widget in [0, 10, 14, 15] { + io.connect_widget(widget, xbridge.widget(widget).unwrap()); + } + let weak_io = Arc::downgrade(&io); + xbridge.set_interrupt_sender(Arc::new(move |packet| { + if packet.widget != 10 { + return false; + } + weak_io.upgrade().is_some_and(|io| { + io.inbound_write( + packet.source_widget, + None, + packet.address, + 8, + packet.vector as u64 | if packet.level { 1 << 8 } else { 0 }, + ) == BUS_OK + }) + })); + let weak_io = Arc::downgrade(&io); + xbridge.set_dma_sender(Arc::new(move |packet, bytes| { + if packet.widget != 10 || packet.half_read_requested || packet.half_write_requested { + dlog!(LogModule::Bedrock, + "Fuel: unsupported XBridge DMA destination or halfword placement {packet:?}" + ); + return false; + } + let Some(io) = weak_io.upgrade() else { + return false; + }; + // Preserve the width of XBridge's naturally aligned packet legs, + // including Hub registers whose narrower accesses are refused. + let size = bytes.len(); + if matches!(size, 1 | 2 | 4 | 8) + && packet.address & (size as u64 - 1) == 0 + { + if packet.write { + let value = bytes.iter().fold(0u64, |value, byte| (value << 8) | *byte as u64); + return io.inbound_write( + packet.source_widget, Some(packet.slot), packet.address, size as u8, value, + ) == BUS_OK; + } + let value = io.inbound_read( + packet.source_widget, Some(packet.slot), packet.address, size as u8, + ); + if value.status != BUS_OK { + return false; + } + bytes.copy_from_slice(&value.data.to_be_bytes()[8 - size..]); + return true; + } + for (i, byte) in bytes.iter_mut().enumerate() { + let a = packet.address + i as u64; + if packet.write { + if io.inbound_write(packet.source_widget, Some(packet.slot), a, 1, *byte as u64) + != BUS_OK + { + return false; + } + } else { + let value = io.inbound_read(packet.source_widget, Some(packet.slot), a, 1); + if value.status != BUS_OK { + return false; + } + *byte = value.data as u8; + } + } + true + })); + let mac = l1.station_mac(); + let mut ioc3_config = Ioc3Config::fuel(mac); + ioc3_config.keyboard_mouse = ps2_input; + let ioc3 = Arc::new(Ioc3::new(ioc3_config)); + let eth = Ioc3Eth::new_fuel(mac); + ioc3.attach_eth(eth.clone()); + ioc3.attach_timekeeper(timekeeper); + ioc3.set_serial_backend(0, serial[1].clone()); + ioc3.set_serial_backend(1, serial[2].clone()); + let scsi = Arc::new(Isp12160::new( + Isp12160::new_bus(), + Isp12160::new_bus(), + )); + xbridge.attach(15, 1, scsi.clone()); + xbridge.attach(15, 4, ioc3.clone()); + let usb = Ohci::new(); + usb.set_clock(clock.clone()); + if usb_input { + // Host input reaches the USB devices through the same controller + // object the GUI, VNC and CI already drive; each device drops + // what its guest has not enabled. + let keyboard = UsbKeyboard::new(); + let mouse = UsbMouse::new(); + usb.attach(0, keyboard.clone())?; + usb.attach(1, mouse.clone())?; + ioc3.ps2().add_sink(keyboard); + ioc3.ps2().add_sink(mouse); + } + xbridge.attach(15, 5, usb.clone()); + for slot in [1, 4, 5] { + xbridge.set_pci_irq(15, slot, 0, xbridge.pin_line(15, slot)); + } + ioc3.connect_superio_interrupt(xbridge.pin_line(15, 0)); + let reset_ioc3 = ioc3.clone(); + let reset_eth = eth.clone(); + let reset_scsi = scsi.clone(); + let reset_usb = usb.clone(); + xbridge.set_reset_output(Arc::new(move |port| { + if port == 15 { + reset_ioc3.power_on(); + reset_eth.power_on(); + reset_scsi.power_on(); + reset_usb.power_on(); + } + })); + let mut bus = Ip35Bus::new(phys.clone()); + #[cfg(feature = "jitv2")] + bus.set_ram_generations(md.dma_view()); + md.rebuild_memory(); + bus.set_fallback(1, 1 << 32, Arc::new(IoPio(io.clone()))); + // The two XIO register forms select the requester's PI0 interface. + let permission_md = Arc::downgrade(&md); + let permission: Arc bool + Send + Sync> = Arc::new(move || { + permission_md.upgrade().is_some_and(|md| md.current_cell() & 1 != 0) + }); + let protected_regs = Arc::new(Window::new(0, 0x80_0000, registers.clone()).permitted(permission.clone())); + for base in [0x0100_0000, 0x0180_0000] { + bus.map(1, base, 0x80_0000, protected_regs.clone()); + } + // Cached RAM uses MD's direct bank mappings; diagnostic views stay separate. + bus.map(3, 0, 1 << 33, md.uncached_view()); + // Hub-special low memory: its first 256 MiB is the node memory view. + bus.map(0, 0, 0x1000_0000, md.uncached_view()); + let prom = Arc::new( + flash + .array_view(0, PART_SIZE) + .map_err(|e| e.to_string())?, + ); + let boot = Arc::new(flash.boot_view()); + for base in [0x1800_0000, 0x3800_0000] { + bus.map(0, base, 0x800_0000, boot.clone()); + } + // Reset execution always sees stored bytes, independently of commands. + bus.map(0, 0x1fc0_0000, 0x40_0000, prom.clone()); + if !payload.is_empty() { + map_device( + &phys, + 0x1fc0_0000, + payload.len() as u64, + prom.clone(), + "Fuel cached reset PROM", + ); + } + let leds = Arc::new(Leds { + md: md.clone(), + values: Mutex::new([0; 4]), + }); + // The three windows occupy the same processor register page. + let special = Arc::new(HubSpecialPage { + windows: [ + Window::new(0, 0x80, Arc::new(HubHead)).permitted(permission.clone()), + Window::new(0x80, 0x40, l1.clone()).permitted(permission.clone()), + Window::new(0xc0, 0x20, leds).permitted(permission.clone()), + ], + }); + for base in [0x1000_0000, 0x3000_0000] { + bus.map(0, base, 1 << SLOT_SHIFT, special.clone()); + bus.map(0, base + 0x400_0000, 8, Arc::new(BoardId(md.clone()))); + } + bus.map(0, 0x1_c000_0000, 1 << 30, md.directory_bus()); + bus.map(0, 0x1_4000_0000, 1 << 30, md.ecc_bus()); + bus.map(0, 0x1_0000_0000, 1 << 30, md.ecc_scrub_bus()); + bus.map(2, 0, 1 << 33, md.fetchop_bus()); + Ok(Self { + bus: Arc::new(bus), + pi, + md, + l1, + flash, + lb, + io, + ni, + xb, + xbridge, + ioc3, + eth, + scsi, + usb, + serial, + #[cfg(test)] + irq, + last_clock: AtomicU64::new(clock()), + clock, + running: AtomicBool::new(false), + }) + } + + pub fn cached_bus(&self) -> Arc { + self.bus.cached_view(self.md.dma_view()) + } + + pub fn build_cpu(&self, part: &Part) -> Box { + let mut cpu = build_cpu(self.bus.clone(), part); + cpu.cache = R12000ShadowCache::new(self.cached_bus()); + // The PROM keeps its stack in cache lines it tags for a window with no + // memory behind it yet; the controller answers that window while they stand. + let memory = self.md.clone(); + cpu.cache.set_pd_tag_store_hook(Box::new(move |tag| memory.startup_tag_store(tag))); + cpu + } + + pub fn start(&self) { + self.last_clock + .store((self.clock)(), Ordering::Release); + self.md.start_clock(); + self.running + .store(true, Ordering::Release); + for pi in &self.pi { + pi.start(); + } + self.l1.start(); + self.ioc3.start(); + self.eth.start(); + self.scsi.start(); + self.usb.start(); + } + pub fn poll(&self) { + if self.running.load(Ordering::Acquire) { + let now = (self.clock)(); + let previous = self + .last_clock + .swap(now, Ordering::AcqRel); + self.flash.advance_ns(now.saturating_sub(previous)); + self.usb.sync_clock(); + for pi in &self.pi { + pi.poll(); + } + } + } + pub fn stop(&self) { + self.poll(); + self.running + .store(false, Ordering::Release); + self.md.stop_clock(); + self.usb.stop(); + self.scsi.stop(); + self.eth.stop(); + self.ioc3.stop(); + self.l1.stop(); + for pi in &self.pi { + pi.stop(); + } + } +} + +// XIO register packets reach only the two PI interrupt-modify registers. +// The processor's register projection has a separate, wider interface. +struct IoHub([Arc; 2]); +impl BusDevice for IoHub { + fn write64(&self, a: u64, v: u64) -> u32 { + if matches!(a, 0x90 | 0x20_0090) && self.0[(a >> 21) as usize].io_modify(v, 0) { + BUS_OK + } else { + BUS_ERR + } + } +} + +struct HubSpecialPage { + windows: [Window; 3], +} +impl HubSpecialPage { + fn target(&self, a: u64, n: u64) -> Option<&Window> { + self.windows.iter().find(|window| window.contains(a, n)) + } +} +macro_rules! hub_page_access { + ($r:ident,$w:ident,$t:ty,$out:ident,$n:expr) => { + fn $r(&self, a: u64) -> $out { + self.target(a, $n).map(|window| window.$r(a)).unwrap_or_else($out::err) + } + fn $w(&self, a: u64, value: $t) -> u32 { + self.target(a, $n).map(|window| window.$w(a, value)).unwrap_or(BUS_ERR) + } + }; +} +impl BusDevice for HubSpecialPage { + hub_page_access!(read8, write8, u8, BusRead8, 1); + hub_page_access!(read16, write16, u16, BusRead16, 2); + hub_page_access!(read32, write32, u32, BusRead32, 4); + hub_page_access!(read64, write64, u64, BusRead64, 8); +} + +struct BoardId(Arc); +impl BusDevice for BoardId { + fn read64(&self, a: u64) -> BusRead64 { + if a == 0 && self.0.current_cell() & 1 != 0 { + BusRead64::ok(FUEL_BOARD_ID) + } else { + BusRead64::err() + } + } +} +// Compatibility head windows: their register contract is not established. +// PROM 6.210 writes 0x2000 at offsets 0 and 0x40 before DRAM discovery. +// Retain the existing zero-read/acknowledged-write model, with accesses +// observable through the Bedrock log, until the register behavior is known. +struct HubHead; +macro_rules! head_access { + ($r:ident,$w:ident,$ty:ty,$result:ident) => { + fn $r(&self, a: u64) -> $result { + dlog!(LogModule::Bedrock, + "Fuel: unidentified Hub-special read {} bytes at {a:#x}", + std::mem::size_of::<$ty>() + ); + $result::ok(0) + } + fn $w(&self, a: u64, v: $ty) -> u32 { + dlog!(LogModule::Bedrock, + "Fuel: unidentified Hub-special write {} bytes at {a:#x} value {v:#x}", + std::mem::size_of::<$ty>() + ); + BUS_OK + } + }; +} +impl BusDevice for HubHead { + head_access!(read8, write8, u8, BusRead8); + head_access!(read16, write16, u16, BusRead16); + head_access!(read32, write32, u32, BusRead32); + head_access!(read64, write64, u64, BusRead64); +} +struct Leds { + md: Arc, + values: Mutex<[u8; 4]>, +} +impl BusDevice for Leds { + fn read64(&self, a: u64) -> BusRead64 { + if a < 32 && a & 7 == 0 && self.md.current_cell() & 1 != 0 { + BusRead64::ok(self.values.lock()[a as usize / 8] as u64) + } else { + BusRead64::err() + } + } + fn write64(&self, a: u64, v: u64) -> u32 { + if a < 32 && a & 7 == 0 && self.md.current_cell() & 1 != 0 { + self.values.lock()[a as usize / 8] = v as u8; + BUS_OK + } else { + BUS_ERR + } + } +} + +struct IoPio(Arc); +macro_rules! pio_access { + ($r:ident,$w:ident,$ty:ty,$result:ident,$size:expr) => { + fn $r(&self, a: u64) -> $result { + let r = self.0.pio_read(a, $size); + $result { + data: r.data as $ty, + status: r.status, + } + } + fn $w(&self, a: u64, v: $ty) -> u32 { + self.0.pio_write(a, $size, v as u64) + } + }; +} +impl BusDevice for IoPio { + pio_access!(read8, write8, u8, BusRead8, 1); + pio_access!(read16, write16, u16, BusRead16, 2); + pio_access!(read32, write32, u32, BusRead32, 4); + pio_access!(read64, write64, u64, BusRead64, 8); +} +impl Drop for FuelBoard { + fn drop(&mut self) { + self.stop(); + } +} + +#[cfg(test)] +#[derive(Default)] +pub(crate) struct PendingIrq(AtomicU64); +#[cfg(test)] +impl PendingIrq { + pub fn bits(&self) -> u64 { + self.0.load(Ordering::Acquire) + } +} +#[cfg(test)] +impl InterruptController for PendingIrq { + fn set_line(&self, line: u32, level: bool) { + if line >= 5 { + return; + } + let mask = 1 << (10 + line); + if level { + self.0.fetch_or(mask, Ordering::Release); + } else { + self.0 + .fetch_and(!mask, Ordering::Release); + } + } +} +struct UartIrq([Arc; 2]); +impl InterruptController for UartIrq { + fn set_line(&self, _: u32, level: bool) { + for pi in &self.0 { + pi.set_uart_irq(level); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8db3414a3..5c22a5278 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,9 @@ pub mod ip32_machine; /// The SGI Octane (IP30): its bus and machine -- see `docs/Octane/ip30-address-map.md`. pub mod ip30; pub mod ip30_machine; +/// Fuel has an IP34 board and uses the IP35 software platform and processor views. +pub mod ip35; +pub mod ip35_machine; pub mod machine_state; pub mod platform; pub mod physical; diff --git a/src/machine.rs b/src/machine.rs index 74f73c831..1239204fb 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -53,7 +53,7 @@ pub fn set_emulator_profile(profile: MachineProfile) { fn emulator_profile() -> Option { let raw = EMULATOR_PROFILE.load(std::sync::atomic::Ordering::Relaxed); - [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30] + [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::FuelIp34] .into_iter().find(|p| *p as u8 == raw) } @@ -68,6 +68,7 @@ fn emulator_name_thirds(profile: Option) -> &'static [&'static s Some(MachineProfile::Indigo2Ip28) => &["IRIX", "Iris", "IPsomething", "Indigo2", "IP28"], Some(MachineProfile::O2Ip32) => &["IRIX", "Iris", "IPsomething", "IP32"], Some(MachineProfile::OctaneIp30) => &["IRIX", "Iris", "IPsomething", "IP30"], + Some(MachineProfile::FuelIp34) => &["IRIX", "Iris", "IPsomething", "IP34"], None => &["IRIX", "Iris", "IPsomething"], } } @@ -2168,11 +2169,12 @@ mod emulator_name_tests { Some(MachineProfile::Indigo2Ip28) => &["Indy", "IP24", "IP22", "IP32", "O2"], Some(MachineProfile::O2Ip32) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "O2"], Some(MachineProfile::OctaneIp30) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP32", "O2"], + Some(MachineProfile::FuelIp34) => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], None => &["Indy", "Indigo2", "IP24", "IP22", "IP28", "IP30", "IP32", "O2"], } }; for p in [None, Some(MachineProfile::IndyIp24), Some(MachineProfile::Indigo2Ip22), - Some(MachineProfile::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30)] { + Some(MachineProfile::Indigo2Ip28), Some(MachineProfile::O2Ip32), Some(MachineProfile::OctaneIp30), Some(MachineProfile::FuelIp34)] { let thirds = emulator_name_thirds(p); assert!(!thirds.is_empty()); for w in thirds { @@ -2184,7 +2186,7 @@ mod emulator_name_tests { #[test] fn profile_round_trips_through_the_global() { - for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30] { + for p in [MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22, MachineProfile::Indigo2Ip28, MachineProfile::O2Ip32, MachineProfile::OctaneIp30, MachineProfile::FuelIp34] { set_emulator_profile(p); assert_eq!(emulator_profile(), Some(p)); } diff --git a/src/workstation.rs b/src/workstation.rs index 0d5537a80..8ea301da6 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -40,6 +40,7 @@ use crate::traits::{Device, MachineEvent}; pub fn build(cfg: MachineConfig) -> Result, String> { crate::machine::set_emulator_profile(cfg.machine.profile); match cfg.machine.profile { + crate::config::MachineProfile::FuelIp34 => Ok(Box::new(crate::ip35_machine::FuelMachine::new(&cfg)?)), // The O2 is its own machine (CRIME, MACE, GBE), not an MC/HPC3 one. crate::config::MachineProfile::O2Ip32 => Ok(Box::new(crate::ip32_machine::O2Machine::new(&cfg)?)), // So is the Octane (HEART, Xbow, Bridge). From bc460f6ed8a072c979b5ddc754b09e402a41d507 Mon Sep 17 00:00:00 2001 From: James Braid Date: Sat, 31 Oct 2026 08:26:55 -0700 Subject: [PATCH 17/20] jitv2: compile the reset PROM from the PROM, not the RAM over it The IP35 PROM fetches its own code uncached at plain physical 0x1fcxxxxx. Bedrock serves that from the reset PROM, but the compile pool read through the cached view, and once a memory bank covers the window the cached view answers RAM there. The memory test zeroes it, so the JIT compiled pages of NOPs in place of PROM routines and the PROM faulted (an instruction fetch bus error, or endless refills). Code pages are keyed by physical frame, so the cached and uncached fetches of that address were one page. Make them two addresses, the way the board decodes them. An uncached instruction fetch of the reset window is now presented to the bus at the same PROM at an address of its own (MipsExecutor::set_uncached_fetch_alias, called by the Fuel; RESET_WINDOW_FETCH, above every RAM slot). The JIT stays as it is: it keys, reads and tracks that page by address like any other, the flash device's generation counter invalidates it, and a store to RAM over the window never does. The alias wraps the translation the CPU installs when it is set, so a cached fetch, a data access and every other machine's fetch path are untouched. The evidence is partial. Which space a request reaches follows the processor's uncached attribute (ip35-address-map.md, processor-side address spaces, established), so an uncached and a cached access of one address are different requests. What attribute 0 returns at 0x1fc00000 is not established: no public source says how the reset request reaches flash, and the cached side there is open too. The split rests on what PROM 6.210's memory test needs, and the code and CHANGELOG say so. The compile pool restarted after a page-pool flush used the CPU's uncached bus instead of the cached plane it was started with; the executor now keeps the compile bus (set_jitv2_code_bus) and passes it to every restart and to the inline compile. The monitor uses it too: `j2 inline off`, `dumppcp`, `corpus` and `j2 html` read pages through it. Tests: an uncached reset-vector fetch compiles the PROM's ADDIU and the cached frame compiles RAM's boundary word, and a flash write invalidates only the PROM's page; a pool restart after a flush keeps the compile bus; a cached fetch and an unaliased executor key the frame they name; uncached attributes 1 to 3 and addresses past the end of memory get no code page. --- CHANGELOG.md | 10 ++ HACKING.md | 10 ++ src/cpu/mips_exec.rs | 111 +++++++++++++++- src/cpu/mips_exec/monitor.rs | 16 +-- src/ip35.rs | 59 ++++++++- src/ip35/tests.rs | 250 +++++++++++++++++++++++++++++++++++ src/ip35_machine.rs | 28 +++- 7 files changed, 465 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7637ea30f..ad05133d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -327,6 +327,16 @@ 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. +### SGI Fuel (IP34 board, IP35 software platform) + +- **jitv2 compiles the reset PROM from the PROM.** The PROM runs from the reset + window uncached; once memory covers that window the cached view returns RAM + there, and the compile pool used to compile RAM's bytes in place of the + PROM's. The board now presents an uncached instruction fetch of that window at + an address of the PROM's own (`MipsExecutor::uncached_fetch_alias`), so its + code page is keyed, compiled and invalidated by address like any other. The + other machines are unchanged. + ### PCI configuration - A PCI function can describe its own writable header fields and diff --git a/HACKING.md b/HACKING.md index a74f8e56e..57830f44b 100644 --- a/HACKING.md +++ b/HACKING.md @@ -653,6 +653,16 @@ code into host code with Cranelift. The concepts: pfn map publishes a slot after its claim, and inline stores bump generations atomically. Compiled code is freed only by a full flush, and every flush parks all CPUs first (`jitv2_flush_from_here`, `run_leader_flush`). +- **Uncached fetches the board decodes apart.** A page is keyed by the + physical address of the fetch. The Fuel's reset PROM answers an uncached fetch + at 0x1fc00000 whatever RAM sits behind the cached view there, so the board + presents that fetch at an address of its own (`uncached_fetch_alias`, set by + `FuelBoard::build_cpu`) and the JIT sees two different frames. Compiles read + that frame through the executor's compile bus (`set_jitv2_code_bus`, + `Ip35Bus::code_view`), and its generation counter is the PROM device's, so a + flash command invalidates it and a store to RAM never does. The alias wraps the + translation (`update_translate_fn` installs the wrapper when one is set), so + the other machines' fetch paths are the ones they always had. - Compiled pages can optionally be cached on disk across runs (`[jitv2] cache`, [docs/jitv2-persistent-cache.md](docs/jitv2-persistent-cache.md)). diff --git a/src/cpu/mips_exec.rs b/src/cpu/mips_exec.rs index df2db98b1..86c28137e 100644 --- a/src/cpu/mips_exec.rs +++ b/src/cpu/mips_exec.rs @@ -1210,6 +1210,10 @@ pub struct MipsExecutor { /// short loop). #[cfg(feature = "jitv2")] pub jitv2_inline_compile: bool, + /// The bus compiles read code through, when it is not `sysad` itself + /// (see [`Self::set_jitv2_code_bus`]). + #[cfg(feature = "jitv2")] + jitv2_code_bus: Option>, /// Scratch analyzer for `jitv2_inline_compile`'s synchronous path — same /// `comp::handle_request` compile sequence the real async compile-thread /// runs, just called directly on the CPU thread. Kept alive across calls @@ -1256,6 +1260,15 @@ pub struct MipsExecutor { /// reach EntryLo and come out as physical address bits 32 and up. All /// ones (no fold) by default; each machine sets its own. pub phys_mask: u64, + /// A window of physical addresses the board decodes differently for an + /// uncached instruction fetch, `(base, size, target)`: the fetch is + /// presented to the bus at `target` plus the offset into the window. The + /// Fuel's reset PROM answers uncached fetches at 0x1fc00000 whatever the + /// cached view holds there, and gives that device an address of its own, + /// so the code page for it is keyed, read and invalidated by address like + /// any other. None on the other machines, whose uncached and cached + /// instruction fetches reach the same bytes. + uncached_fetch_alias: Option>>, /// Where the machine has memory, from [`MipsCpuConfig::memory`]. pub memory: Arc, /// Frames the JIT's page table has, the end of `memory` in pages. Kept @@ -1379,6 +1392,31 @@ fn translate_64_user(e: &mut MipsExecutor, va: u64, at e.translate_64bit_impl::(va, at) } +/// A window whose uncached instruction fetch the board decodes apart, and the +/// translation it wraps (`MipsExecutor::set_uncached_fetch_alias`). +struct FetchAlias { + base: u64, + size: u64, + target: u64, + inner: fn(&mut MipsExecutor, u64, AccessType) -> TranslateResult, +} + +/// The translation wrappers above for a board that decodes an uncached +/// instruction fetch apart. Only a fetch that comes out uncached is changed. +fn translate_fetch_aliased(e: &mut MipsExecutor, va: u64, at: AccessType) -> TranslateResult { + let inner = e.uncached_fetch_alias.as_ref().map_or(translate_32_kernel:: as fn(&mut MipsExecutor, u64, AccessType) -> TranslateResult, |a| a.inner); + let mut result = inner(e, va, at); + if at == AccessType::Fetch && !result.is_exception() && !result.is_cached() { + if let Some(alias) = e.uncached_fetch_alias.as_ref() { + let offset = result.phys.wrapping_sub(alias.base); + if offset < alias.size { + result.phys = alias.target + offset; + } + } + } + result +} + /// Free-standing trampoline for the CP0 Status callback installed by `install_status_cb`. /// Rust does not allow `Self` inside a nested fn, so the generic trampoline lives here. // Safety: the executor's raw pointers point into allocations owned by @@ -2807,6 +2845,8 @@ impl MipsExecutor { #[cfg(feature = "jitv2")] jitv2_inline_compile: cfg!(feature = "jitv2_lockstep"), #[cfg(feature = "jitv2")] + jitv2_code_bus: None, + #[cfg(feature = "jitv2")] jitv2_inline_analyzer: crate::cpu::jitv2::analyzer::Analyzer::with_isa(C::MIPS4), // IRIS_JIT_DISPATCH=off starts with the JIT dispatch gate shut: the // same binary as an interpreter-only reference, for comparing @@ -2817,6 +2857,7 @@ impl MipsExecutor { batch_hook: None, phys_alias: None, phys_mask: u64::MAX, + uncached_fetch_alias: None, memory: cfg.memory.clone(), #[cfg(feature = "jitv2")] jitv2_frames: crate::cpu::jitv2::jitv2::frames_for(&cfg.memory), @@ -3382,7 +3423,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", #[cfg(feature = "jitv2")] pub(crate) fn jitv2_flush_from_here(&mut self, only_if_exhausted: bool) { crate::pf_scope!(crate::perfetto::events::JIT_CPU_FLUSH); - let bus = self.sysad.clone(); + let bus = self.jitv2_compile_bus(); let jit = self.jitv2.clone(); let flush = || { let mut j = jit.lock(); @@ -3411,6 +3452,28 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", paddr, self.memory)) } + /// Tell the JIT which bus compiles read code through, when it is not the + /// CPU's own: the Fuel reads cached pages through its cached plane, and + /// its reset PROM's page through the uncached view that holds it + /// (`uncached_fetch_alias`). Call while the executor is being built, + /// before it is shared or run. + #[cfg(feature = "jitv2")] + pub fn set_jitv2_code_bus(&mut self, code_bus: Arc) { + self.jitv2_code_bus = Some(code_bus); + } + + /// The code page the last fetch landed on, or null. + #[cfg(feature = "jitv2")] + pub fn jitv2_current_page(&self) -> *mut crate::cpu::jitv2::PhysicalCodePage { + self.pcp + } + + /// The bus compile workers and the inline compile read code through. + #[cfg(feature = "jitv2")] + pub fn jitv2_compile_bus(&self) -> Arc { + self.jitv2_code_bus.clone().unwrap_or_else(|| self.sysad.clone()) + } + #[cfg(feature = "jitv2")] fn jitv2_share(&mut self, jit: Arc>, world: Arc) { { @@ -3458,7 +3521,9 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", #[inline(always)] fn jitv2_track_pcp(&mut self, phys_addr: u64) { // Code above the JIT's pfn map gets no page and runs in the - // interpreter (`step_jit` checks for the null page). + // interpreter (`step_jit` checks for the null page). That includes + // uncached attributes 1 to 3: their bits 58:57 sit in `phys_addr`, + // so the frame range check rejects them. let Some(pfn) = crate::cpu::jitv2::jitv2::pfn_of(phys_addr, self.jitv2_frames) else { self.clear_pcp(); return; @@ -3587,7 +3652,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", let is_64bit = self.core.is_64bit_mode(); self.core.lwr_upper_below = if C::PARTIAL_LWR_KEEPS_UPPER && is_64bit { 3 } else { 0 }; let privilege = self.core.get_privilege_mode(); - self.translate_fn = match (is_64bit, privilege) { + let translate_fn = match (is_64bit, privilege) { (false, PrivilegeMode::Kernel) => translate_32_kernel::, (false, PrivilegeMode::Supervisor) => translate_32_supervisor::, (false, PrivilegeMode::User) => translate_32_user::, @@ -3595,6 +3660,37 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", (true, PrivilegeMode::Supervisor) => translate_64_supervisor::, (true, PrivilegeMode::User) => translate_64_user::, }; + self.translate_fn = translate_fn; + // A board that decodes an uncached instruction fetch apart wraps the + // translation itself, so the fetch paths of the other machines stay + // as they are. + if self.uncached_fetch_alias.is_some() { + self.wrap_translate_fn(translate_fn); + } + } + + /// `update_translate_fn`'s choice, wrapped for `uncached_fetch_alias`. + /// Out of line so the update itself stays as small as it was; the wrapper + /// reaches the choice through a pointer so that the translation bodies keep + /// the one caller they inline into. + #[cold] + #[inline(never)] + fn wrap_translate_fn(&mut self, inner: fn(&mut Self, u64, AccessType) -> TranslateResult) { + if let Some(alias) = self.uncached_fetch_alias.as_mut() { + alias.inner = inner; + } + self.translate_fn = translate_fetch_aliased::; + } + + /// Present an uncached instruction fetch of the window `base..base+size` + /// to the bus at `target` plus the offset (see [`Self::uncached_fetch_alias`]). + pub fn set_uncached_fetch_alias(&mut self, base: u64, size: u64, target: u64) { + // Never called: `update_translate_fn` replaces it below. + fn placeholder(_: &mut MipsExecutor, _: u64, _: AccessType) -> TranslateResult { + unreachable!("the translation wrapper is installed before it is used") + } + self.uncached_fetch_alias = Some(Box::new(FetchAlias { base, size, target, inner: placeholder:: })); + self.update_translate_fn(); } /// Re-derive `fpr_read_d/write_d/read_l/write_l` from STATUS_FR. @@ -9317,7 +9413,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", .is_some_and(|c| c.packing_stats().1 > crate::cpu::jitv2::CODEGEN_ARENA_FLUSH_THRESHOLD_BYTES); if over_threshold { let phys_page_base = page.pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; - unsafe { self.jitv2.lock().flush_from_cpu_thread(self.sysad.clone()); } + unsafe { self.jitv2.lock().flush_from_cpu_thread(self.jitv2_compile_bus()); } self.clear_pcp(); self.jitv2_track_pcp(phys_page_base); return EXEC_RETRY; @@ -9328,6 +9424,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", "jitv2_lockstep: inline compile found no Codegen (async compile queue stole it?) — \ verification would silently be OFF. The queue must not run under lockstep."); let mut ran_out_of_memory = false; + let compile_bus = self.jitv2_compile_bus(); if let Some(codegen) = codegen.as_mut() { // Stamp the L1-D geometry the inline load/store fast path needs. // Done on every take rather than once at startup so a cache @@ -9339,10 +9436,10 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", #[cfg(feature = "developer")] { let stats = self.jitv2.lock().stats.clone(); - ran_out_of_memory = crate::cpu::jitv2::comp::handle_request(&req, &self.sysad, &mut self.jitv2_inline_analyzer, codegen, &stats); + ran_out_of_memory = crate::cpu::jitv2::comp::handle_request(&req, &compile_bus, &mut self.jitv2_inline_analyzer, codegen, &stats); } #[cfg(not(feature = "developer"))] - { ran_out_of_memory = crate::cpu::jitv2::comp::handle_request(&req, &self.sysad, &mut self.jitv2_inline_analyzer, codegen); } + { ran_out_of_memory = crate::cpu::jitv2::comp::handle_request(&req, &compile_bus, &mut self.jitv2_inline_analyzer, codegen); } } *self.jitv2.lock().codegen.lock() = codegen; { @@ -9353,7 +9450,7 @@ va={:#018x} phys={:#010x} (code pfn {:#x}, page {:#010x}, word {}/{})", if ran_out_of_memory { let page = unsafe { &mut *self.pcp }; let phys_page_base = page.pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; - unsafe { self.jitv2.lock().flush_from_cpu_thread(self.sysad.clone()); } + unsafe { self.jitv2.lock().flush_from_cpu_thread(self.jitv2_compile_bus()); } self.clear_pcp(); self.jitv2_track_pcp(phys_page_base); return EXEC_RETRY; diff --git a/src/cpu/mips_exec/monitor.rs b/src/cpu/mips_exec/monitor.rs index a4385fad1..12172078d 100644 --- a/src/cpu/mips_exec/monitor.rs +++ b/src/cpu/mips_exec/monitor.rs @@ -2027,7 +2027,7 @@ impl MipsCpu { #[cfg(not(feature = "jitv2_lockstep"))] { exec.jitv2_inline_compile = false; - let bus = exec.sysad.clone(); + let bus = exec.jitv2_compile_bus(); let old = exec.jitv2.lock().codegen.lock().take(); match old { Some(old) => { @@ -2557,7 +2557,7 @@ impl MipsCpu { return Err(format!("phys {:#x} is above the JIT's physical range", result.phys)); }; let page_base = pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let mut jit = exec.jitv2.lock(); let fr1 = (exec.core.cp0_status & crate::cpu::mips_core::STATUS_FR) != 0; let slot = jit.page_for(pfn, page_base, sysad.as_ref(), fr1); @@ -2567,7 +2567,7 @@ impl MipsCpu { } }; let page = unsafe { &*page_ptr }; - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let dump = crate::cpu::jitv2::pcp_dump::PcpDump::capture(page, sysad.as_ref()) .map_err(|st| format!("capture failed: bus status {:#x}", st))?; // A path argument is whichever arg is not the address. @@ -2601,7 +2601,7 @@ impl MipsCpu { // the bus, and holding the jitv2 lock across a few // thousand of those plus the filesystem would stall // every dispatching CPU thread for the whole dump. - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let mut unreadable = 0usize; let dumps: Vec = { let jit = exec.jitv2.lock(); @@ -2700,7 +2700,7 @@ impl MipsCpu { return Ok(()); }; let page_base = pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let fr1 = (exec.core.cp0_status & crate::cpu::mips_core::STATUS_FR) != 0; let mut jit = exec.jitv2.lock(); match jit.page_for(pfn, page_base, sysad.as_ref(), fr1) { @@ -2862,7 +2862,7 @@ impl MipsCpu { drop(symbols); let pfn = exec.jitv2_monitor_pfn(paddr)?; let page_base = pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let fr1 = (exec.core.cp0_status & crate::cpu::mips_core::STATUS_FR) != 0; let mut jit = exec.jitv2.lock(); match jit.page_for(pfn, page_base, sysad.as_ref(), fr1) { @@ -2910,7 +2910,7 @@ impl MipsCpu { let pfn = exec.jitv2_monitor_pfn(paddr)?; let page_base = pfn as u64 * crate::cpu::jitv2::PAGE_SIZE as u64; let offset = (paddr & 0xFFF) as usize >> 2; - let sysad = exec.sysad.clone(); + let sysad = exec.jitv2_compile_bus(); let fr1 = (exec.core.cp0_status & crate::cpu::mips_core::STATUS_FR) != 0; let mut jit = exec.jitv2.lock(); match jit.page_for(pfn, page_base, sysad.as_ref(), fr1) { @@ -3010,7 +3010,7 @@ impl MipsCpu { // implementation. let path = actual_args.get(1).copied().unwrap_or("jitv2.html"); let jitv2_arc = exec.jitv2.clone(); - let bus = exec.sysad.clone(); + let bus = exec.jitv2_compile_bus(); crate::cpu::jitv2_html_j2wp::write_jitv2_html( &jitv2_arc, &bus, &exec.memory.clone(), &mut exec.jitv2_inline_analyzer, path, &mut *writer, )?; diff --git a/src/ip35.rs b/src/ip35.rs index ff2af349f..128a02bfc 100644 --- a/src/ip35.rs +++ b/src/ip35.rs @@ -23,6 +23,25 @@ use std::sync::Arc; pub(crate) const CPU_PHYS_MASK: u64 = ((1u64 << 40) - 1) | (3u64 << 57); const PA_MASK: u64 = (1u64 << 40) - 1; +/// The reset PROM's compatibility window: the processor's reset vector and +/// the PROM's own code are at 0x1fc00000, fetched uncached. +pub(crate) const RESET_WINDOW: u64 = 0x1fc0_0000; +pub(crate) const RESET_WINDOW_SIZE: u64 = 0x40_0000; +/// Where an uncached instruction fetch of the reset window is presented to +/// the bus (`MipsExecutor::uncached_fetch_alias`): the same PROM at an +/// address of its own, above every RAM slot and below the 16 GiB the JIT's +/// frame map covers. +/// +/// The split is inferred. The hardware selects a space by the processor's +/// uncached attribute (established: ip35-address-map.md, "Processor-side +/// address spaces"), so an uncached attribute-0 access and a cached one are +/// different requests to Bedrock. What attribute 0 returns at 0x1fc00000 is +/// not established: no public source says how the reset request reaches flash +/// (ip35-address-map.md, reset vector), and the cached side is likewise open. +/// The reset PROM answering uncached fetches while cached accesses reach RAM +/// is what PROM 6.210's memory test needs, and that is all this decode rests on. +pub(crate) const RESET_WINDOW_FETCH: u64 = 0x2_0000_0000; + /// Where the Fuel has memory: four 1 GiB slots Bedrock's memory controller /// can place its two 256 MiB banks in, depending on how the PROM programs /// it, and the reset PROM between the first two. Uncached fetches of the @@ -34,7 +53,8 @@ pub fn memory_map() -> crate::memory_map::MemoryMap { let base = slot as u64 * BANK_SPAN; MemRange::new(name, MemKind::Ram, base, base + FITTED_BANK) }).collect(); - ranges.push(MemRange::new("prom", MemKind::Rom, 0x1fc0_0000, 0x2000_0000)); + ranges.push(MemRange::new("prom", MemKind::Rom, RESET_WINDOW, 0x2000_0000)); + ranges.push(MemRange::new("prom (uncached fetch)", MemKind::Rom, RESET_WINDOW_FETCH, RESET_WINDOW_FETCH + RESET_WINDOW_SIZE)); MemoryMap::new(ranges) } @@ -237,6 +257,13 @@ impl Ip35Bus { let pa = plain_address(a, n)?; Some((&self.views[(a >> 57) as usize], pa)) } + /// The bus the JIT compiles code through: the cached view, except the + /// reset PROM's uncached-fetch window ([`RESET_WINDOW_FETCH`]), which only + /// the uncached view holds. + #[cfg(feature = "jitv2")] + pub fn code_view(self: &Arc, cached: Arc) -> Arc { + Arc::new(CodeViews { cached, uncached: self.clone() }) + } pub fn cached_view(&self, plain_memory: Arc) -> Arc { Arc::new(CachedPhysical { phys: self.phys.clone(), @@ -251,6 +278,29 @@ fn plain_address(a: u64, n: u64) -> Option { let pa = a & PA_MASK; (pa.checked_add(n)? <= PA_MASK + 1).then_some(pa) } +/// Whether `a` is in the reset PROM's uncached-fetch window. +fn in_fetch_window(a: u64) -> bool { + a.wrapping_sub(RESET_WINDOW_FETCH) < RESET_WINDOW_SIZE +} +/// See [`Ip35Bus::code_view`]. Compiles read instruction words only. +#[cfg(feature = "jitv2")] +struct CodeViews { + cached: Arc, + uncached: Arc, +} +#[cfg(feature = "jitv2")] +impl BusDevice for CodeViews { + fn read32(&self, a: u64) -> BusRead32 { + if in_fetch_window(a) { + self.uncached.read32(a) + } else { + self.cached.read32(a) + } + } + fn gen_ptr(&self, a: u64) -> *const AtomicU64 { + if in_fetch_window(a) { self.uncached.gen_ptr(a) } else { self.cached.gen_ptr(a) } + } +} struct CachedPhysical { phys: Arc, plain_memory: Arc, @@ -295,6 +345,13 @@ macro_rules! processor_access { impl BusDevice for Ip35Bus { #[cfg(feature = "jitv2")] fn gen_ptr(&self, address: u64) -> *const AtomicU64 { + if in_fetch_window(address) { + // The reset PROM's page: its generation is the PROM device's, so + // a flash command invalidates it and a store to RAM never does. + // The counter covers the whole flash, so any flash write + // invalidates every PROM page (over-invalidation, safe). + return self.views[0].gen_ptr(address); + } if address & !PA_MASK == 0 { // JIT page lookup passes plain physical code addresses, without // a cache attribute. Raw UC0 therefore cannot select the MMIO map diff --git a/src/ip35/tests.rs b/src/ip35/tests.rs index 32b246431..ef54b4918 100644 --- a/src/ip35/tests.rs +++ b/src/ip35/tests.rs @@ -640,3 +640,253 @@ fn the_fuel_plugs_a_usb_keyboard_and_mouse_only_when_configured_on() { let (_, status) = ports(false); assert_eq!(status.map(|s| s & 1), [0, 0], "nothing plugged in"); } + +/// The reset PROM fetches uncached at the same plain address that cached RAM +/// answers once memory covers its window. The board presents that fetch at the +/// PROM's own address (`uncached_fetch_alias`), so the JIT keys the page by +/// address and compiles the PROM's bytes; it used to read the cached view and +/// compile the RAM's bytes in place of the PROM's. +#[cfg(feature = "jitv2")] +#[test] +fn uncached_fetch_compiles_the_prom_not_the_ram_over_it() { + // The executor and the code generator are large; the default test stack is not. + std::thread::Builder::new() + .stack_size(64 << 20) + .spawn(uncached_fetch_compiles_the_prom_body) + .unwrap() + .join() + .expect("thread panicked"); +} + +#[cfg(feature = "jitv2")] +fn uncached_fetch_compiles_the_prom_body() { + use crate::cpu::jitv2::jitv2::CompileRequest; + use crate::cpu::mips_isa::{JIT_REGION_BOUNDARY_SENTINEL, OP_ADDIU}; + + /// Every word the same; a region-boundary word compiles to nothing. + struct Words { + word: u32, + generation: AtomicU64, + } + impl Words { + fn new(word: u32) -> Arc { + Arc::new(Self { word, generation: AtomicU64::new(0) }) + } + } + impl BusDevice for Words { + fn read32(&self, _: u64) -> BusRead32 { + BusRead32::ok(self.word) + } + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::ok((self.word as u64) << 32 | self.word as u64) + } + fn gen_ptr(&self, _: u64) -> *const AtomicU64 { + &self.generation + } + } + + let reset_vector = 0x1fc0_0000u64; + let addiu_r1 = (OP_ADDIU << 26) | (1 << 16) | 1; + let rom = Words::new(addiu_r1); + let ram = Words::new(JIT_REGION_BOUNDARY_SENTINEL); + let physical = Physical::new(Vec::new()); + let mut bus = Ip35Bus::new(physical.clone()); + map_device(&physical, reset_vector, 0x1000, ram.clone(), "RAM over the reset window"); + bus.set_ram_generations(ram.clone()); + bus.map(0, reset_vector, 0x1000, rom.clone()); + bus.map(0, RESET_WINDOW_FETCH, 0x1000, rom.clone()); + let bus = Arc::new(bus); + let code_bus = bus.code_view(bus.cached_view(ram.clone())); + // The two views really disagree here. + assert_eq!(bus.read32(reset_vector).data, addiu_r1); + assert_eq!(bus.cached_view(ram.clone()).read32(reset_vector).data, JIT_REGION_BOUNDARY_SENTINEL); + + let mut cpu = crate::ip35_machine::build_cpu(bus.clone(), crate::ip35_machine::CPUS[0]); + alias_the_reset_window(&mut cpu); + cpu.set_jitv2_code_bus(code_bus.clone()); + // The reset vector, KSEG1: uncached, plain physical address 0x1fc00000. + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = cpu.jitv2_current_page(); + assert!(!page.is_null(), "the uncached fetch must land on a code page"); + let page = unsafe { &mut *page }; + assert_eq!(page.pfn as u64, RESET_WINDOW_FETCH / 4096, "an uncached fetch of the reset window keys the PROM's own frame"); + + page.mark_requested(0); + let req = CompileRequest { page: page as *mut _, compiled_for_fr1: true }; + let mut analyzer = crate::cpu::jitv2::analyzer::Analyzer::new(); + let mut codegen = crate::cpu::jitv2::codegen::Codegen::new(); + #[cfg(feature = "developer")] + crate::cpu::jitv2::comp::handle_request(&req, &cpu.jitv2_compile_bus(), &mut analyzer, &mut codegen, &crate::cpu::jitv2::JitStats::default()); + #[cfg(not(feature = "developer"))] + crate::cpu::jitv2::comp::handle_request(&req, &cpu.jitv2_compile_bus(), &mut analyzer, &mut codegen); + assert!(page.is_runnable(0), "the page must compile the PROM's ADDIU, not the RAM's boundary word"); + + // The cached view of the same frame is another page, with RAM's counter and bytes. + let jitv2 = cpu.jitv2.clone(); + let mut jit = jitv2.lock(); + let slot = jit.page_for(0x1fc00, 0x1fc0_0000, bus.as_ref(), false).unwrap(); + let cached = unsafe { &mut *jit.page_ptr(slot) }; + assert_ne!(cached as *mut _, page as *mut _); + cached.mark_requested(0); + let req = CompileRequest { page: cached as *mut _, compiled_for_fr1: true }; + #[cfg(feature = "developer")] + crate::cpu::jitv2::comp::handle_request(&req, &code_bus, &mut analyzer, &mut codegen, &crate::cpu::jitv2::JitStats::default()); + #[cfg(not(feature = "developer"))] + crate::cpu::jitv2::comp::handle_request(&req, &code_bus, &mut analyzer, &mut codegen); + assert!(!cached.is_runnable(0), "the cached page compiles RAM's boundary word to nothing"); + + // A write through one view invalidates only the pages that view's bytes feed. + ram.generation.fetch_add(1, std::sync::atomic::Ordering::Release); + assert!(page.is_runnable(0), "a cached-view write does not change the PROM's bytes"); + rom.generation.fetch_add(1, std::sync::atomic::Ordering::Release); + assert!(!page.is_runnable(0), "a write to the PROM's bytes invalidates its code"); +} + +#[cfg(feature = "jitv2")] +fn alias_the_reset_window(cpu: &mut crate::ip35_machine::FuelCpu) { + cpu.set_uncached_fetch_alias(RESET_WINDOW, RESET_WINDOW_SIZE, RESET_WINDOW_FETCH); +} + +#[cfg(feature = "jitv2")] +mod code_views { + use super::*; + use crate::cpu::jitv2::jitv2::CompileRequest; + use crate::cpu::mips_isa::{JIT_REGION_BOUNDARY_SENTINEL, OP_ADDIU}; + + struct Words(u32, AtomicU64); + impl BusDevice for Words { + fn read32(&self, _: u64) -> BusRead32 { + BusRead32::ok(self.0) + } + fn read64(&self, _: u64) -> BusRead64 { + BusRead64::ok((self.0 as u64) << 32 | self.0 as u64) + } + fn gen_ptr(&self, _: u64) -> *const AtomicU64 { + &self.1 + } + } + + const RESET: u64 = 0x1fc0_0000; + const ADDIU_R1: u32 = (OP_ADDIU << 26) | (1 << 16) | 1; + + /// A bus whose uncached attribute-0 view of the reset frame holds an ADDIU + /// and whose cached view holds a region boundary, as RAM over the PROM does. + fn bus_and_cpu(aliased: bool) -> (Arc, Box) { + let rom = Arc::new(Words(ADDIU_R1, AtomicU64::new(0))); + let ram = Arc::new(Words(JIT_REGION_BOUNDARY_SENTINEL, AtomicU64::new(0))); + let physical = Physical::new(Vec::new()); + let mut bus = Ip35Bus::new(physical.clone()); + map_device(&physical, RESET, 0x1000, ram.clone(), "RAM over the reset window"); + bus.set_ram_generations(ram.clone()); + bus.map(0, RESET, 0x1000, rom.clone()); + // Code that is fetchable in the other views and past the end of memory, so only + // the JIT's own range check keeps it out of a code page. + for attribute in 1..4 { + bus.map(attribute, RESET, 0x1000, rom.clone()); + } + bus.map(0, RESET_WINDOW_FETCH, 0x1000, rom.clone()); + bus.map(0, 0x3_0000_0000, 0x1000, rom); + let bus = Arc::new(bus); + let mut cpu = crate::ip35_machine::build_cpu(bus.clone(), crate::ip35_machine::CPUS[0]); + if aliased { + let cached = bus.cached_view(ram); + alias_the_reset_window(&mut cpu); + cpu.set_jitv2_code_bus(bus.code_view(cached)); + } + (bus, cpu) + } + + /// The executor and the code generator are large; the default test stack is not. + fn big_stack(body: fn()) { + std::thread::Builder::new().stack_size(64 << 20).spawn(body).unwrap().join().expect("thread panicked"); + } + + fn wait_runnable(page: &crate::cpu::jitv2::PhysicalCodePage) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while std::time::Instant::now() < deadline { + if page.is_runnable(0) { return true; } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + false + } + + /// After a page-pool flush the restarted compile pool must still read the + /// PROM's page through the compile bus. Restarted on the CPU's own bus it + /// rejects the view address and the page never compiles. + #[test] + fn a_pool_restart_after_a_flush_keeps_the_compile_bus() { + big_stack(|| { + let (_bus, mut cpu) = bus_and_cpu(true); + { + let jit = cpu.jitv2.clone(); + let mut g = jit.lock(); + let stats = g.stats.clone(); + g.compile_queue.set_thread_count(1); + g.compile_queue.start(cpu.jitv2_compile_bus(), stats); + } + cpu.jitv2_flush_from_here(false); + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &mut *cpu.jitv2_current_page() }; + assert_eq!(page.pfn as u64, RESET_WINDOW_FETCH / 4096); + page.mark_requested(0); + let jit = cpu.jitv2.clone(); + let g = jit.lock(); + let stats = g.stats.clone(); + assert!(g.compile_queue.send(CompileRequest { page: page as *mut _, compiled_for_fr1: true }, &stats)); + drop(g); + assert!(wait_runnable(page), "the restarted pool must compile the PROM's page"); + jit.lock().compile_queue.stop(); + }); + } + + /// With no alias an uncached fetch keys the frame it names, as on every + /// other machine; and with it a cached fetch of the same window still does. + #[test] + fn a_cached_fetch_and_an_unaliased_executor_key_the_frame_they_name() { + big_stack(|| { + let (_bus, mut cpu) = bus_and_cpu(false); + cpu.core.pc = 0xffff_ffff_bfc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &*cpu.jitv2_current_page() }; + assert_eq!(page.pfn, 0x1fc00); + + let (_bus, mut cpu) = bus_and_cpu(true); + // KSEG0: cached, the same physical address. + cpu.core.pc = 0xffff_ffff_9fc0_0000; + let _ = cpu.step_int(); + let page = unsafe { &*cpu.jitv2_current_page() }; + assert_eq!(page.pfn, 0x1fc00, "a cached fetch is not presented at the PROM's frame"); + }); + } + + /// Uncached attributes 1 to 3 and anything past the end of memory stay in the interpreter. + #[test] + fn attributes_one_to_three_and_high_addresses_get_no_code_page() { + big_stack(|| { + for pc in [ + 0x9000_0000_0000_0000u64 | RESET, // attribute 0: a page + 0x9200_0000_0000_0000u64 | RESET, // attribute 1 + 0x9400_0000_0000_0000u64 | RESET, // attribute 2 + 0x9600_0000_0000_0000u64 | RESET, // attribute 3 + 0x9000_0003_0000_0000u64, // past the end of memory, attribute 0 + ] { + // A fresh executor each time: a tracked page would survive a + // fetch that wrongly kept the same frame. + let (_bus, mut cpu) = bus_and_cpu(true); + // KX, so XKPHYS addresses translate. + let status = cpu.core.cp0_status | 0x80; + cpu.set_cp0_status(status); + cpu.core.pc = pc; + let _ = cpu.step_int(); + let page = cpu.jitv2_current_page(); + if pc == 0x9000_0000_0000_0000u64 | RESET { + assert!(!page.is_null(), "attribute 0 below 4 GiB is compiled"); + } else { + assert!(page.is_null(), "{pc:#x} must not get a code page"); + } + } + }); + } +} diff --git a/src/ip35_machine.rs b/src/ip35_machine.rs index 66a46ca34..87434a4b9 100644 --- a/src/ip35_machine.rs +++ b/src/ip35_machine.rs @@ -139,8 +139,11 @@ impl FuelMachine { // After the windows above: each compile worker takes the cache's // geometry and the CPU model's policies once, as it starts, and // without the windows the geometry has no inline load or store path. - // Workers read plain physical code addresses, which the cached plane - // serves; the uncached views would decode them as Hub registers. + // Workers read plain physical code addresses through the view the CPU + // fetched each page by: the cached plane for cached pages, the + // uncached attribute-0 view for the reset PROM's uncached fetches. + // Decoding plain addresses in the uncached views for cached pages + // would reach Hub registers. #[cfg(feature = "jitv2")] { let cpu_device: Arc = cpu.clone().as_device(); @@ -155,7 +158,7 @@ impl FuelMachine { #[cfg(not(feature = "jitv2_lockstep"))] { let stats = g.stats.clone(); - g.compile_queue.start(board.cached_bus(), stats); + g.compile_queue.start(board.code_bus(), stats); } // The compile pool keeps only a weak handle on the CPU. std::mem::forget(cpu_device); @@ -836,6 +839,9 @@ impl FuelBoard { } // Reset execution always sees stored bytes, independently of commands. bus.map(0, 0x1fc0_0000, 0x40_0000, prom.clone()); + // The same PROM where an uncached instruction fetch of that window is + // presented (`MipsExecutor::uncached_fetch_alias`). + bus.map(0, crate::ip35::RESET_WINDOW_FETCH, crate::ip35::RESET_WINDOW_SIZE, prom.clone()); if !payload.is_empty() { map_device( &phys, @@ -893,9 +899,25 @@ impl FuelBoard { self.bus.cached_view(self.md.dma_view()) } + /// The bus the JIT compiles code through; see [`Ip35Bus::code_view`]. + #[cfg(feature = "jitv2")] + pub fn code_bus(&self) -> Arc { + self.bus.code_view(self.cached_bus()) + } + pub fn build_cpu(&self, part: &Part) -> Box { let mut cpu = build_cpu(self.bus.clone(), part); cpu.cache = R12000ShadowCache::new(self.cached_bus()); + // The PROM fetches its own code uncached; with memory over its window + // the cached plane answers RAM there, so the fetch is presented at the + // PROM's own address and compiled from the view that holds it. + cpu.set_uncached_fetch_alias( + crate::ip35::RESET_WINDOW, + crate::ip35::RESET_WINDOW_SIZE, + crate::ip35::RESET_WINDOW_FETCH, + ); + #[cfg(feature = "jitv2")] + cpu.set_jitv2_code_bus(self.code_bus()); // The PROM keeps its stack in cache lines it tags for a window with no // memory behind it yet; the controller answers that window while they stand. let memory = self.md.clone(); From 0f332549a2b47a42137097afe487dc8af596376f Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 04:22:12 -0700 Subject: [PATCH 18/20] odyssey: let a widget-offset machine host the board, with credit accounting The board assumed the Octane: its interrupt packets went only to HEART at widget 8, and it took the processor's addresses in the IP30 widget windows. A Fuel's processor reaches widget 13 through Bedrock, which hands the board widget offsets, and the board's packets cross the XBridge's crossbar to Bedrock like any widget's. Add a constructor for such a machine: it takes a packet sender for the interrupts and decodes every address as an aperture offset. The Octane's constructor and behaviour are unchanged. A test routes a packet to a widget other than HEART and requires the sender to get it, not HEART's line. Graphics credits (odyssey): A Bedrock or Hub processor interface counts graphics credits: each counted store to a graphics head takes one, and the head returns it as the store leaves its command FIFO. The Octane's HEART has no such block, so the board left the accounting out. Bring it back for a machine that links a processor interface: the PiGfx register block and the PiGfxLink trait (return, owe and repay credits), a counted and an owed mark on each queued store, the credit loop in retire and clear, the one-store runs while a counted store waits, and the processor's one-shot store mark. With no link, as on the Octane, every credit call is a no-op. How long a V12 takes to return a credit is not published. The host's credit window is 100 doublewords (IP35) or 256 (IP30), a credit is inferred to come back when the board takes the doubleword into its FIFO, and the FIFO must hold more than the window. The model credits a store as it enters while fewer than 16,384 stores wait (cfifo::DEPTH). The initialise call stores its start-up program, about 12,291 words, with no level check before the low-water interrupt is routed; with a depth of 512 two boots in five timed out with "CFIFO timeout" and no screens, and 16,384 is the smallest power of two that covers the program even at one word per store. The depth is a model choice derived from that program. A deeper store owes its credit and hurries the board thread; a reset refunds what the abandoned stores owed. Inside the depth the board thread takes full runs, so a credited backlog does not drain one store at a time. The board thread reads the count of counted stores once per run taken and once per run retired, on every machine. Both reads are Relaxed: a run's length is only a hint, a stale count costs a longer run and never a lost credit, and the retire reads it under the lock a counted push takes to change it. Nothing else on the Octane's store path changed. The tests drive the credit rules with a stand-in link: a counted store is credited on entry, a reset cancels owed credits, the mark counts exactly one store, a widget that runs out of credits starves, the FIFO set-up words do not widen the credit depth, and an owed credit hurries the board thread. Others fill the largest host window plus one with the board taking nothing and require every credit back with none owed, and post 16,384 counted stores and require each credited at entry and the next one owed. The refused-push test used to sleep 150 ms and require a push thread to be unfinished, which a loaded host could break once the board thread's timed hold ran out first. The hold now lasts until the test releases it, and the test waits for the push to be refused before it asserts. --- clanker/odyssey/gotchas.md | 3 +- clanker/odyssey/threads-and-locks.md | 23 +- src/dev/odyssey/board.rs | 1 + src/dev/odyssey/consts.rs | 27 +- src/dev/odyssey/fifo.rs | 192 ++++++++++++-- src/dev/odyssey/interface.rs | 56 +++- src/dev/odyssey/mmio.rs | 56 +++- src/dev/odyssey/mod.rs | 84 ++++-- src/dev/odyssey/pi_gfx.rs | 210 +++++++++++++++ src/dev/odyssey/tests/display.rs | 4 +- src/dev/odyssey/tests/fifo.rs | 379 ++++++++++++++++++++++++--- src/dev/odyssey/tests/interrupts.rs | 23 ++ src/dev/odyssey/worker.rs | 11 +- 13 files changed, 959 insertions(+), 110 deletions(-) create mode 100644 src/dev/odyssey/pi_gfx.rs diff --git a/clanker/odyssey/gotchas.md b/clanker/odyssey/gotchas.md index 826d3672f..7d7db173b 100644 --- a/clanker/odyssey/gotchas.md +++ b/clanker/odyssey/gotchas.md @@ -40,7 +40,8 @@ Status: rule (2026-10-30). timing measured to better than that is noise. - **Model choices to remember.** The FIFO's depth is unknown in any unit. - The model clamps the level at 32,768 words, whatever the stored `0x6bX` + The model keeps 16,384 stores for + credits and clamps the level at 32,768 words, whatever the stored `0x6bX` values say, and uses a 16-word low-water threshold; `0x6b2` as the status-bit-15 threshold is a model choice too. The level is in words, a doubleword store counting two. The tests pin the model, not the diff --git a/clanker/odyssey/threads-and-locks.md b/clanker/odyssey/threads-and-locks.md index c34604524..15831f5fb 100644 --- a/clanker/odyssey/threads-and-locks.md +++ b/clanker/odyssey/threads-and-locks.md @@ -38,7 +38,7 @@ apply to BUZZ). Nothing models more than one raster unit. - **Command FIFO** (`fifo.rs`, CPU to `odyssey-work`): the processor's stores to the two command-FIFO ports and the context-switch stores, each - as (offset, size, value). The level the guest sees + as (offset, size, value) with its credit state. The level the guest sees is in words, against the model's 32,768. - **Raster queue** (`raster_unit.rs`, `odyssey-work` to `odyssey-raster`): jobs, each owning what its raster work reads, captured when the front @@ -115,8 +115,8 @@ other, as MGRAS's producers take its `submit` lock (below). drawing. A processor access to a host-interface register does not take it. - **`HostInterface`** is behind the front lock, a register file's own lock held for a few instructions: identity, control, the interrupt bank, - the routing, the swap bank and the FIFO status, plus the interrupt state. - A front access (those registers) takes only it + the routing, the swap bank and the FIFO status, plus the interrupt state + and the credit return. A front access (those registers) takes only it and never waits for a draw. It is a leaf; the raster thread takes it for a fence's interrupt, and the display takes it to latch swaps. - **The raster unit's state** (`RasterCtx`: the stipple phase and the rows @@ -199,15 +199,15 @@ the room check and the push, is refused, and its store retries the push behind t (`BoardLocks::write_command`), not around it. The ring is deeper than the board's FIFO, so a second producer's overshoot of the level by a store is absorbed. The consumer (the worker, or a draining access with the worker stopped) looks at up to 32 stores, runs them, and takes the ones it ran out together -(`CommandFifo::take_run`, `retire`): the level and the store count -move once a run, not once a store, because each store costs the +(`CommandFifo::take_run`, `retire`): the level, the store count and the +credits move once a run, not once a store, because each store costs the worker little and the processor reads those words. A store that has run but not retired still counts in the level, so the level never reads below what the -board has left to take. A push counts its store (level, store count) -before the ring shows it and takes the counts back off if the ring +board has left to take. A push counts its store (level, store count, counted +stores) before the ring shows it and takes the counts back off if the ring refuses it: the worker can take a store and retire it the instant it lands, before the push returns, and a retire must find every store it takes already -counted or the level goes below zero. +counted or the level goes below zero. While a counted store waits a run is one store. While the worker runs every command store queues, so decoding and drawing never run on the processor's thread. @@ -227,6 +227,13 @@ A caller other than the bus, with the board's thread stopped, still runs the queue and then its store. The flow-control target word `+0x1074` is stored, not routed, so a store to it goes in at once even while the FIFO holds work. +Counted stores (machines whose processor interface counts graphics credits; +the Octane has none and the calls are no-ops) are credited as they enter the +FIFO's first 16,384 places (the model's depth, a model choice), counted in +stores: at the store when there is room, +or when the stores ahead have run, with the node holding the credit as owed +meanwhile. + ## The FIFO ordering rules These are guest-visible, and hold across both queues; any change to either diff --git a/src/dev/odyssey/board.rs b/src/dev/odyssey/board.rs index 888426ca9..0beae2656 100644 --- a/src/dev/odyssey/board.rs +++ b/src/dev/odyssey/board.rs @@ -837,6 +837,7 @@ impl Board { // established, so it is zero. let identity = self.identity(); let mut f = self.front.lock(); + f.slot = self.slot; f.identity = identity; f.widget_control = self.slot & ODSY_CONTROL_WIDGET_MASK; f.xt_req_timeout = ODSY_XT_REQ_TIMEOUT_RESET; diff --git a/src/dev/odyssey/consts.rs b/src/dev/odyssey/consts.rs index c79b996d0..0e72cf83e 100644 --- a/src/dev/odyssey/consts.rs +++ b/src/dev/odyssey/consts.rs @@ -179,8 +179,9 @@ pub(super) mod reg { // the bit and raises the flag, and zero, as out of reset, is no threshold. // Neither the roles nor the link between bit 15, flag 25 and any 0x6bX value // is established. The level's unit (32-bit words, a doubleword store counting - // two) is a model choice, as is the FIFO's size (super::cfifo::WORDS), - // independent of the stored values, and so is the low-water threshold (super::cfifo::LOW_WATER). The model contract is pinned by + // two) is a model choice, as is the FIFO's size (super::cfifo::WORDS) and depth + // (super::cfifo::DEPTH), independent of the stored values, and so is the + // low-water threshold (super::cfifo::LOW_WATER). The model contract is pinned by // the_level_counts_words_against_the_model_fifo_and_the_6b2_threshold and // the_threshold_follows_6b2. pub const FIFO_SETUP_FIRST: u32 = 0x6b0; @@ -319,6 +320,28 @@ pub(super) mod cfifo { // deeper or shallower. Test, a model contract: // the_fifo_size_ignores_the_set_up_words. pub const WORDS: u32 = 32768; + + // The waiting stores whose credits have been returned, in stores (one + // credit per store). MODEL CHOICE: the FIFO's depth is UNKNOWN (see + // WORDS). It must exceed the host's credit window of 100 (IP35) or 256 + // (IP30) doublewords, and a credit is inferred to come back when the + // board takes the doubleword into its FIFO. Exceeding the window is not + // enough: the + // kernel's initialise call stores its start-up program, about 12,291 + // words, with no level check, before the low-water interrupt has a + // destination, and a credit owed during that load can leave the writer + // held until the 15-second FIFO timer. 16,384 is the smallest power of two + // that covers the whole program even at one word per store, so none of it + // waits on the board's thread for its credit. The figure is a model choice + // derived from the documented ~12,291-word start-up program, as evidence + // for a depth whose grade is unknown. Limitation: past this depth a credit + // waits for the board's thread again, the processor can run this many + // stores ahead of the board, and a writer that does is held until the + // backlog drains below the low-water mark. The stored 0x6bX values do not + // change it (fifo.rs). Tests: the_credit_depth_is_deeper_than_the_host_window, + // every_store_inside_the_credited_depth_is_credited_and_the_next_is_owed, + // the_fifo_set_up_does_not_widen_the_credit_depth (a model contract). + pub const DEPTH: u32 = 16384; } pub(super) const ODSY_FIFO_SETUP_7B5_INDEX: usize = 4; diff --git a/src/dev/odyssey/fifo.rs b/src/dev/odyssey/fifo.rs index f582e8cf0..41f47e2e9 100644 --- a/src/dev/odyssey/fifo.rs +++ b/src/dev/odyssey/fifo.rs @@ -40,10 +40,39 @@ //! //! The processor's stores are the producer; whoever runs the queue (the //! board's thread, or an access that drains it with that thread stopped) -//! is the consumer. +//! is the consumer. Graphics credits, which only machines whose processor +//! interface counts stores use, are settled as a store enters the FIFO's +//! first `cfifo::DEPTH` places: at the store when there is room, or +//! when the stores ahead of it have run. That depth (16,384 stores) is a +//! MODEL CHOICE, independent of the stored `0x6bX` values: the smallest +//! power of two that holds the kernel's whole start-up program (about +//! 12,291 words) even at one word per store, and far deeper than the +//! largest host credit window (256). A writer is then credited as its stores enter and does +//! not wait on the board's thread. That matters because IRIX's high-water +//! handler looks at the count for only about ten microseconds before it +//! holds the writer, and while the X server's initialise call is loading +//! that program, before the low-water interrupt has a destination, only +//! the kernel's 15-second FIFO timer wakes a writer held there. Limitation: a writer that runs more than the depth plus its +//! window ahead of the board waits for the board's thread again, and is +//! held until the level falls below the low-water mark. Whether a 32-bit +//! store costs the hardware a whole credit is UNKNOWN; the model credits +//! one per store. +//! +//! The deeper credited FIFO widens what the guest can see. A counted writer +//! can now leave up to the depth plus its window (about 16,640 stores) +//! waiting, where 64 plus the window was the bound before, so the level, +//! the unreturned-credit field of `0x106c` (which saturates), status bit 17 +//! (below the low-water mark) and flag 9 follow a backlog that long: bit 17 +//! stays clear, and flag 9 latches, only after it drains. Doubleword stores +//! that deep reach the `0x6b2` threshold the kernel stores, 32,192 words, +//! which the old bound never did, so status bit 15 and flag 25 can now +//! rise for counted stores too; the kernel leaves flag 25 masked and does +//! not read bit 15. The level the guest reads stops at the model's FIFO +//! size. use super::consts::{cfifo, intr, ODSY_WORK_QUEUE_MAX}; use super::interface::HostInterface; +use super::pi_gfx::{pi_gfx_owe_credits, pi_gfx_repay_credits}; use super::worker::GAP; use std::cell::Cell; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::{Relaxed, SeqCst}}; @@ -63,18 +92,28 @@ pub(super) struct QueuedStore { pub value: u64, pub addr: u32, pub size: u8, + /// The processor interface counted it: its credit is due. + pub counted: bool, + /// It waited outside the FIFO, so the node holds its credit as owed. + pub owed: bool, } // A ring entry is (address word, value). The address word carries the -// aperture offset in its low 24 bits and the store's size above them. +// aperture offset in its low 24 bits and the store's size and credit +// state above them. const ENTRY_OFFSET_MASK: u32 = 0x00ff_ffff; const ENTRY_SIZE_SHIFT: u32 = 24; const ENTRY_SIZE_MASK: u32 = 0xf; +const ENTRY_COUNTED: u32 = 1 << 28; +const ENTRY_OWED: u32 = 1 << 29; impl QueuedStore { fn entry(&self) -> u32 { debug_assert!(self.addr <= ENTRY_OFFSET_MASK && self.size as u32 <= ENTRY_SIZE_MASK); - self.addr | (self.size as u32) << ENTRY_SIZE_SHIFT + self.addr + | (self.size as u32) << ENTRY_SIZE_SHIFT + | if self.counted { ENTRY_COUNTED } else { 0 } + | if self.owed { ENTRY_OWED } else { 0 } } fn from_entry(addr: u32, value: u64) -> Self { @@ -82,6 +121,8 @@ impl QueuedStore { value, addr: addr & ENTRY_OFFSET_MASK, size: ((addr >> ENTRY_SIZE_SHIFT) & ENTRY_SIZE_MASK) as u8, + counted: addr & ENTRY_COUNTED != 0, + owed: addr & ENTRY_OWED != 0, } } @@ -107,7 +148,7 @@ pub(super) struct Run { impl Run { pub fn new() -> Self { - Run { stores: [QueuedStore { value: 0, addr: 0, size: 0 }; RUN_MAX], len: 0 } + Run { stores: [QueuedStore { value: 0, addr: 0, size: 0, counted: false, owed: false }; RUN_MAX], len: 0 } } /// The stores to run, oldest first. @@ -132,6 +173,8 @@ struct Levels { /// second its submissions reach (`DisplayShared::covered`). pushed: AtomicU64, retired: AtomicU64, + /// Counted stores waiting; while zero no store needs credit work. + counted: AtomicU32, } pub(super) struct CommandFifo { @@ -151,9 +194,9 @@ pub(super) struct CommandFifo { /// The board's thread is parked (`wait_for_work`): a producer unparks it /// only then, so a store reads a line nobody writes instead of locking. asleep: CachePadded, - /// The processor waits for the board's thread, for a refused access: its - /// next slice starts without the pause for a burst of stores, and a - /// pause under way ends. + /// The processor waits for the board's thread, for a refused access or + /// a counted store whose credit is owed: its next slice starts without + /// the pause for a burst of stores, and a pause under way ends. hurry: CachePadded, /// The board's thread is running stores. pub slice: CachePadded, @@ -174,7 +217,13 @@ pub(super) struct CommandFifo { /// counted after its last look at the hurry request. #[cfg(test)] pub test_parked: AtomicU32, - /// Tests: the next push calls this, once, as soon as its + /// Tests: pushes the full ring has refused. + #[cfg(test)] + pub test_refused: AtomicU32, + /// Tests: ends a long slice's hold (`Board::test_slice_sleep_ms`) early. + #[cfg(test)] + pub test_release: AtomicBool, + /// Tests: the next uncounted push calls this, once, as soon as its /// store is in the ring: a consumer that takes the store at that /// moment, before the push has returned. #[cfg(test)] @@ -188,7 +237,7 @@ impl Default for CommandFifo { // place because the ring is too big for the stack. ring: unsafe { Box::new_zeroed().assume_init() }, pop: Mutex::new(()), - levels: CachePadded::new(Levels { words: AtomicU32::new(0), stores: AtomicU32::new(0), pushed: AtomicU64::new(0), retired: AtomicU64::new(0) }), + levels: CachePadded::new(Levels { words: AtomicU32::new(0), stores: AtomicU32::new(0), pushed: AtomicU64::new(0), retired: AtomicU64::new(0), counted: AtomicU32::new(0) }), threshold_6b2: CachePadded::new(AtomicU32::new(0)), running: CachePadded::new(AtomicBool::new(false)), asleep: CachePadded::new(AtomicBool::new(false)), @@ -205,6 +254,10 @@ impl Default for CommandFifo { #[cfg(test)] test_parked: AtomicU32::new(0), #[cfg(test)] + test_refused: AtomicU32::new(0), + #[cfg(test)] + test_release: AtomicBool::new(false), + #[cfg(test)] test_after_ring_push: Mutex::new(None), } } @@ -271,9 +324,9 @@ impl CommandFifo { self.running.load(SeqCst) && !self.has_room() } - /// The processor waits for the board's thread to run what is queued, as - /// a refused access does. A thread already in its pause (`gap`) is woken - /// to run at once. + /// The processor waits for the board's thread to run what is queued: + /// a refused access, or a counted store whose credit is owed. A thread + /// already in its pause (`gap`) is woken to run at once. pub fn hurry(&self) { // A refused store retries in a loop; write the line only when it // changes, as IMPACT does for its asleep flag. @@ -330,20 +383,36 @@ impl CommandFifo { self.running.load(SeqCst) || self.pending() || self.held() } - /// Queue a store. False when the host ring has no room for it. - pub fn push(&self, front: &Mutex, addr: u64, value: u64, size: u32) -> bool { + /// The stores in the FIFO's first places, credited. + pub fn in_fifo(&self) -> u32 { + self.stores().min(cfifo::DEPTH) + } + + /// Queue a store. A counted store is credited at once when it lands + /// inside the FIFO's first places and owed otherwise. False when the + /// board's FIFO has no room for it. + pub fn push(&self, front: &Mutex, addr: u64, value: u64, size: u32, counted: bool) -> bool { let words = store_words(size); if !self.has_room() { + #[cfg(test)] + self.test_refused.fetch_add(1, SeqCst); return false; } - let q = QueuedStore { value, addr: addr as u32, size: size as u8 }; + let mut q = QueuedStore { value, addr: addr as u32, size: size as u8, counted, owed: false }; + // A counted store's place is settled against the consumer's pops. + let _c = counted.then(|| self.pop.lock()); // Count the store before the ring shows it. The board's thread can // take it and retire it the moment it is in the ring, and a retire // must find every store it takes already counted, or the level falls // below zero. Until the push lands the counts are a store high, // which reads as a store still waiting, never as one gone. - self.levels.stores.fetch_add(1, SeqCst); + if counted { + self.levels.counted.fetch_add(1, SeqCst); + } + let stores_before = self.levels.stores.fetch_add(1, SeqCst); let before = self.levels.words.fetch_add(words, SeqCst); + let credited = counted && stores_before < cfifo::DEPTH; + q.owed = counted && !credited; if !self.ring.try_push(q.entry(), q.value) { // has_room reads the ring's own view and `submit` serialises // the producers, so this should not happen; if it does, the @@ -352,12 +421,33 @@ impl CommandFifo { // come back off. self.levels.words.fetch_sub(words, SeqCst); self.levels.stores.fetch_sub(1, SeqCst); + if counted { + self.levels.counted.fetch_sub(1, SeqCst); + } return false; } self.levels.pushed.fetch_add(1, SeqCst); #[cfg(test)] - if let Some(consumer) = self.test_after_ring_push.lock().take() { - consumer(self, front); + if !counted { + if let Some(consumer) = self.test_after_ring_push.lock().take() { + consumer(self, front); + } + } + if counted { + let mut f = front.lock(); + if credited { + f.return_credit(); + } else { + if let Some(pi) = &f.pi { + pi_gfx_owe_credits(pi.as_ref(), f.slot, 1); + } + drop(f); + // The credit comes back only as the stores ahead retire, and + // the processor can be watching its credit count (a read the + // bus does not refuse), so the board's thread runs without + // its pause. + self.hurry(); + } } let after = before + words; let mark = self.threshold_6b2.load(SeqCst); @@ -373,17 +463,28 @@ impl CommandFifo { } /// Look at the oldest waiting stores, up to `RUN_MAX`, without taking - /// them out; the caller runs them, then `retire`s the ones it ran. + /// them out; the caller runs them, then `retire`s the ones it ran. While + /// stores wait past the credited depth, one of which may owe its credit, + /// the run is one store, so each owed credit is settled as the store + /// ahead of it leaves rather than after a whole run. Inside the depth + /// every counted store already has its credit, so the run is full. pub fn take_run(&self, run: &mut Run) { + // Only a hint for the run's length, so a stale count costs a longer + // run, never a lost credit (`retire` settles each one). The stores + // are read first and short-circuit it: a Relaxed read of `counted` + // after seeing more than DEPTH stores sees every counted push whose + // store count it read, since a push counts `counted` first. + let owing = self.stores() > cfifo::DEPTH && self.levels.counted.load(Relaxed) != 0; + let max = if owing { 1 } else { RUN_MAX }; let mut raw = [(0u32, 0u64); RUN_MAX]; - run.len = self.ring.peek_run_at(0, &mut raw); + run.len = self.ring.peek_run_at(0, &mut raw[..max]); for (store, (addr, value)) in run.stores.iter_mut().zip(&raw[..run.len]) { *store = QueuedStore::from_entry(*addr, *value); } } /// Take the first `n` stores of `run` out of the FIFO: they have run. The - /// level and the stores waiting move once for the run, not + /// level, the stores waiting and the credits move once for the run, not /// once per store, which is what the board's thread spends most of its /// time on when each store costs little to decode. A store the thread has /// run but not retired still counts in the level, so the level never @@ -399,6 +500,7 @@ impl CommandFifo { } let ran = &run.stores[..n]; let words: u32 = ran.iter().map(QueuedStore::words).sum(); + let counted_ran = ran.iter().filter(|q| q.counted).count() as u32; let _c = self.pop.lock(); self.ring.consume_n_at(0, n); self.levels.retired.fetch_add(n as u64, SeqCst); @@ -417,18 +519,60 @@ impl CommandFifo { } else { self.levels.words.fetch_sub(words, SeqCst); } - self.levels.stores.fetch_sub(n as u32, SeqCst); + let stores_before = self.levels.stores.fetch_sub(n as u32, SeqCst); + if counted_ran != 0 { + self.levels.counted.fetch_sub(counted_ran, SeqCst); + } + // `pop` is held, and a counted push takes it to count `counted`, so + // this read needs no ordering of its own. Relaxed keeps the + // machines with no counted stores (every one but a linked + // processor interface) off a sequentially consistent load per run. + if self.levels.counted.load(Relaxed) == 0 { + return; + } + // The next waiting store moves into the FIFO's last credited place + // and is credited. + for k in 1..=n as u32 { + if stores_before - k < cfifo::DEPTH { + continue; + } + // The run is already out of the ring, so count back from its head. + let place = cfifo::DEPTH as usize - 1 - (n - k as usize); + if let Some((addr, value)) = self.ring.peek_nth_at(0, place) { + let entered = QueuedStore::from_entry(addr, value); + let mut f = front.lock(); + if entered.owed { + if let Some(pi) = &f.pi { + pi_gfx_repay_credits(pi.as_ref(), f.slot, 1); + } + } else if entered.counted { + f.return_credit(); + } + } + } } - /// Abandon every waiting store. - pub fn clear(&self) { + /// Abandon every waiting store; an owed one outside the FIFO gives its + /// owed credit back. + pub fn clear(&self, front: &Mutex) { let _c = self.pop.lock(); + let mut index = 0u32; while let Some((addr, value)) = self.ring.peek() { self.ring.consume(); let q = QueuedStore::from_entry(addr, value); self.levels.words.fetch_sub(q.words(), SeqCst); self.levels.stores.fetch_sub(1, SeqCst); self.levels.retired.fetch_add(1, SeqCst); + if q.counted { + self.levels.counted.fetch_sub(1, SeqCst); + } + if q.owed && index >= cfifo::DEPTH { + let f = front.lock(); + if let Some(pi) = &f.pi { + pi_gfx_owe_credits(pi.as_ref(), f.slot, -1); + } + } + index += 1; } } diff --git a/src/dev/odyssey/interface.rs b/src/dev/odyssey/interface.rs index 49727c3a8..39a922c37 100644 --- a/src/dev/odyssey/interface.rs +++ b/src/dev/odyssey/interface.rs @@ -28,13 +28,22 @@ use super::consts::{ use super::display_fifo::display_fifo_access; use super::fifo::CommandFifo; use super::mmio::{access_waits, command_store, fifo_port}; +use super::pi_gfx::{pi_gfx_return_credits, PiGfxLink}; use super::registers::reg; +use super::PacketSender; use parking_lot::Mutex; use std::sync::Arc; pub(super) struct HostInterface { - /// Where interrupt packets go (the iris glue's HEART). + /// The crosstalk widget number. + pub slot: u32, + /// Where interrupt packets go on the Octane (the iris glue's HEART). pub irq: Option>, + /// Where interrupt packets go on a machine whose fabric carries them to + /// any widget (the Fuel's XBridge crossbar); `irq` is unused then. + pub packets: Option, + /// The master node's processor interface, for graphics credits. + pub pi: Option>, /// The widget number of HEART, the packets' destination. pub heart_widget: u32, /// ODSY_IDENTITY, composed from the revision at reset. @@ -80,6 +89,9 @@ pub(super) struct HostInterface { /// The command FIFO (fifo.rs), whose level STATUS0 reports. pub fifo: Arc, + /// Tests: stores count as counted by a processor interface. + #[cfg(test)] + pub test_counted_stores: u8, /// The front accesses' own trace numbering; the board's accesses keep /// the state's. pub trace_sequence: u64, @@ -88,7 +100,10 @@ pub(super) struct HostInterface { impl Default for HostInterface { fn default() -> Self { HostInterface { + slot: 0, irq: None, + packets: None, + pi: None, heart_widget: 8, identity: 0, widget_control: 0, @@ -117,6 +132,8 @@ impl Default for HostInterface { displayed_buffer: 0, displayed_written: false, fifo: Default::default(), + #[cfg(test)] + test_counted_stores: 0, trace_sequence: 0, } } @@ -285,6 +302,25 @@ impl HostInterface { self.fifo.level() } + /// The FIFO's level went from 'before' to its current value. Falling below + /// the model's low-water threshold (cfifo::LOW_WATER, a model choice) + /// latches the low-water flag whatever the enable mask + /// says; it reaches the driver only while the source is enabled. + pub fn fifo_level_changed(&mut self, before: u32) { + if before >= cfifo::LOW_WATER && self.fifo_level() < cfifo::LOW_WATER { + self.intr_flags |= intr::FIFO_LOW_WATER; + self.update_irq(); + } + } + + /// A counted store's credit, to the processor interface the node names. + pub fn return_credit(&mut self) { + if let Some(pi) = &self.pi { + pi_gfx_return_credits(pi.as_ref(), self.slot, 1); + } else { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: counted graphics store has no processor interface for its return"); + } + } } // The front registers. @@ -396,7 +432,7 @@ impl HostInterface { self.flctl_target_reported = true; crate::dlog!( LogModule::Odyssey, - "sgi-odyssey: flow-control interrupt target {} and credit target {} stored", + "sgi-odyssey: flow-control interrupt target {} and credit target {} stored; credits still return through the node interface", (value as u32) >> flctl::INTR_TARGET_SHIFT, ((value >> flctl::TARGET_SHIFT) as u32) & flctl::TARGET_MASK ); @@ -465,19 +501,23 @@ pub(super) fn front_mmio_read(f: &Mutex, addr: u64, size: u32) -> /// A processor's store through the front alone: true when it went to a /// front register or joined the command FIFO for the board's thread, false -/// when it needs the board's state. -pub(super) fn front_mmio_write(f: &Mutex, fifo: &CommandFifo, addr: u64, value: u64, size: u32) -> bool { +/// when it needs the board's state. `counted`: the processor interface counted +/// it. +pub(super) fn front_mmio_write(f: &Mutex, fifo: &CommandFifo, addr: u64, value: u64, size: u32, counted: bool) -> bool { if front_register(addr, size, true) { let mut g = f.lock(); // The store falls after every command store the processor made // before it: a swap arm waits for them (HostInterface::retrace). g.stream_pos = fifo.pushed(); g.write(addr, value, size); + if counted { + g.return_credit(); + } return true; } // A queued store needs the board's thread to run it, and the queue to // have room; otherwise the caller runs it. - command_store(addr, size) && fifo.running.load(std::sync::atomic::Ordering::SeqCst) && fifo.push(f, addr, value, size) + command_store(addr, size) && fifo.running.load(std::sync::atomic::Ordering::SeqCst) && fifo.push(f, addr, value, size, counted) } /// The fabric packet's address bits; the widget sits above them. @@ -504,6 +544,12 @@ impl HostInterface { pub fn send_interrupt(&mut self, packet: u64, number: u8, set: bool) { let widget = (packet >> XIO_PACKET_ADDRESS_BITS) as u32; let address = packet & ((1u64 << XIO_PACKET_ADDRESS_BITS) - 1); + if let Some(send) = &self.packets { + if !send(widget as u8, address, number, set) { + crate::dlog!(LogModule::Odyssey, "odyssey: interrupt packet to widget {widget} address {address:#x} was refused"); + } + return; + } if widget != self.heart_widget || address != HEART_PACKET { crate::dlog!(LogModule::Odyssey, "odyssey: interrupt packet to widget {widget} address {address:#x} has no receiver"); return; diff --git a/src/dev/odyssey/mmio.rs b/src/dev/odyssey/mmio.rs index 5318dd30c..da31c6594 100644 --- a/src/dev/odyssey/mmio.rs +++ b/src/dev/odyssey/mmio.rs @@ -19,6 +19,7 @@ use super::consts::{ use super::display_fifo::{display_fifo_access, DisplayFifo}; use super::fifo::CommandFifo; use super::interface::{front_mmio_read, front_mmio_write, front_register, HostInterface}; +use super::pi_gfx::pi_gfx_take_mark; use super::raster_unit::RasterQueue; use super::reader::DisplayShared; use super::record::{RecHandle, RecSlot, Recorder}; @@ -138,6 +139,10 @@ impl Board { // see the drawing are answered busy until the queue is empty // (BoardLocks::bus_read) or, with no thread running (unit tests), run what // is queued first (Board::work_drain). + // + // The Octane has no PI link (only Hub and Bedrock machines have graphics + // credits), so the front's `pi` stays None there and every credit call is + // skipped. } @@ -232,13 +237,31 @@ impl Board { } } +/// Whether this store is counted: the processor interface's mark. +pub(super) fn store_counted(front: &parking_lot::Mutex) -> bool { + // The Octane has no PI link, so nothing sets the mark there. + #[cfg(test)] + if front.lock().test_counted_stores != 0 { + return true; + } + let _ = front; + pi_gfx_take_mark() +} + impl Board { /// A store to the board's state. pub fn mmio_write(&mut self, addr: u64, value: u64, size: u32) { + let counted = store_counted(&self.front); + self.mmio_write_counted(addr, value, size, counted); + } + + pub fn mmio_write_counted(&mut self, addr: u64, value: u64, size: u32, mut counted: bool) { let fifo = self.fifo.clone(); - let queued = command_store(addr, size) && fifo.queues() && fifo.push(&self.front, addr, value, size); + let queued = command_store(addr, size) && fifo.queues() && fifo.push(&self.front, addr, value, size, counted); if queued { - // With no board thread running the storing caller runs it. + // Credited as it enters the FIFO (CommandFifo::push). With no + // board thread running the storing caller runs it. + counted = false; if !fifo.running.load(std::sync::atomic::Ordering::SeqCst) && !fifo.held() { self.work_drain(addr); } @@ -253,6 +276,10 @@ impl Board { } self.write_dispatch(addr, value, size); } + if counted { + // It went through the FIFO inside the store. + self.front.lock().return_credit(); + } } } @@ -637,7 +664,8 @@ impl BoardLocks { /// answer busy, when it waits for queued stores the board's thread will /// run, or when it is a command store and the host ring is full while /// the board's thread runs: the processor retries the store, taking its - /// interrupts meanwhile, while the board's thread makes room. + /// interrupts meanwhile, while the board's thread makes room. The + /// processor interface's mark stays untaken until the store goes in. /// /// A store that waits is held back for the command FIFO only, not the /// raster queue: the raster unit never shared a lock with the board's @@ -676,9 +704,10 @@ impl BoardLocks { // serialises the producers, so with the board's thread running // the push goes in; `write_command` waits only if it did not. let running = self.fifo.running.load(std::sync::atomic::Ordering::SeqCst); - if !running || !self.fifo.push(&self.front, addr, value, size) { + let counted = store_counted(&self.front); + if !running || !self.fifo.push(&self.front, addr, value, size, counted) { debug_assert!(!running, "the ring refused a store its room check allowed"); - self.write_command(cpu, addr, value, size); + self.write_command(cpu, addr, value, size, counted); } return true; } @@ -695,26 +724,29 @@ impl BoardLocks { /// A command store the caller has classified, holding `submit`: it joins /// the FIFO for the board's thread, else, with no thread, runs here. - fn write_command(&self, cpu: crate::locks::SpinGuard<'_, ()>, addr: u64, value: u64, size: u32) { + /// `counted` is the processor interface's mark, which can be taken only + /// once: a caller that tried the FIFO first hands on what it took. + fn write_command(&self, cpu: crate::locks::SpinGuard<'_, ()>, addr: u64, value: u64, size: u32, counted: bool) { while self.fifo.running.load(std::sync::atomic::Ordering::SeqCst) { // Not for the processor's bus path (`bus_write` answers a // refused push busy): this spin takes no interrupts. A caller // off the bus waits for room, behind the board's thread. - if self.fifo.push(&self.front, addr, value, size) { + if self.fifo.push(&self.front, addr, value, size, counted) { return; } std::thread::yield_now(); } // SAFETY: no board thread is running. - unsafe { self.board(cpu) }.mmio_write(addr, value, size); + unsafe { self.board(cpu) }.mmio_write_counted(addr, value, size, counted); } /// A store to the board's state that is not a command store, holding /// `submit`. fn write_board(&self, cpu: crate::locks::SpinGuard<'_, ()>, addr: u64, value: u64, size: u32) { + let counted = store_counted(&self.front); // SAFETY: the waiting accesses have drained the FIFO, or the board's // thread is stopped. - unsafe { self.board(cpu) }.mmio_write(addr, value, size); + unsafe { self.board(cpu) }.mmio_write_counted(addr, value, size, counted); } /// A store for a caller other than the bus: through the front alone when it @@ -723,12 +755,14 @@ impl BoardLocks { /// for a store that waits. pub fn write(&self, addr: u64, value: u64, size: u32) { if front_register(addr, size, true) { - front_mmio_write(&self.front, &self.fifo, addr, value, size); + let counted = store_counted(&self.front); + front_mmio_write(&self.front, &self.fifo, addr, value, size, counted); return; } let cpu = self.submit.lock(); if command_store(addr, size) { - return self.write_command(cpu, addr, value, size); + let counted = store_counted(&self.front); + return self.write_command(cpu, addr, value, size, counted); } self.write_board(cpu, addr, value, size); } diff --git a/src/dev/odyssey/mod.rs b/src/dev/odyssey/mod.rs index b3dab555f..307fa713f 100644 --- a/src/dev/odyssey/mod.rs +++ b/src/dev/odyssey/mod.rs @@ -42,7 +42,8 @@ //! - `memory`, `scanout`, `reader`, `display`: board memory and planes; the //! marks writers make and the display thread's own reader, with the scanned //! frame and retrace; what reaches the window. -//! - `pixel_dma`: DMA between guest memory and planes. +//! - `pixel_dma`, `pi_gfx`: DMA between guest memory and planes, and graphics +//! credits for machines whose processor interface counts them. //! - `depth_word`: the board's 24-bit float depth word and its offset. //! - `consts`: register, command and field constants. //! @@ -83,6 +84,8 @@ mod interface; mod clip; mod logic; mod mmio; +mod pi_gfx; +pub use pi_gfx::{PiGfx, PI_GFX_REGS}; mod pixel_dma; mod pipe; mod pixel_write; @@ -133,11 +136,17 @@ pub struct Odyssey { thread: Mutex>>, } +/// Carries a board interrupt packet into a machine's crosstalk fabric: the +/// destination widget, the address within it, the vector, and whether the +/// source rose or fell. False when nothing at the destination takes it. +pub type PacketSender = Arc bool + Send + Sync>; + impl Odyssey { - /// A board of `product` (v12, v10, v8, v6) at `widget`: its interrupt - /// packets reach `irq` (HEART), its DMA reaches `mem`. `pool` is the - /// CPU raster unit's workers and tile size, `raster` which unit draws - /// and, for the GPU, the wgpu backend (`[graphics]`). + /// A board of `product` (v12, v10, v8, v6) at `widget` on the Octane: + /// the processor reaches it through the IP30 widget windows, its + /// interrupt packets reach `irq` (HEART), its DMA reaches `mem`. `pool` + /// is the CPU raster unit's workers and tile size, `raster` which unit + /// draws and, for the GPU, the wgpu backend (`[graphics]`). #[allow(clippy::too_many_arguments, reason = "the board's wiring")] pub fn new( widget: u8, @@ -149,6 +158,57 @@ impl Odyssey { raster: (crate::config::RasterUnit, &str), ) -> Result, String> { let mut s = Board::new(product, widget as u32)?; + let rec = Self::recorder()?; + s.guest = Some(Self::recorded_mem(mem, &rec)); + s.front.lock().irq = Some(irq); + Self::with_board(s, widget_small_base(widget), widget_large_base(widget), status, rec, pool, raster) + } + + /// A board of `product` at `widget` on a machine that hands the board + /// widget-relative addresses, as Bedrock's I/O interface does, and + /// carries its interrupt packets through `packets`. + #[allow(clippy::too_many_arguments, reason = "the board's wiring")] + pub fn new_widget_relative( + widget: u8, + product: &str, + packets: PacketSender, + mem: Arc, + status: Arc, + pool: (usize, u32), + raster: (crate::config::RasterUnit, &str), + ) -> Result, String> { + let mut s = Board::new(product, widget as u32)?; + let rec = Self::recorder()?; + s.guest = Some(Self::recorded_mem(mem, &rec)); + s.front.lock().packets = Some(packets); + // Every address is already an aperture offset: no large window. + Self::with_board(s, 0, u64::MAX, status, rec, pool, raster) + } + + /// The recorder's slot: there whether or not a recording runs, so the + /// monitor can start one (`vpro rec`); the DMA path records through it. + /// `IRIS_ODSY_REC` starts a recording at once. + fn recorder() -> Result { + Ok(record::RecSlot::new(match std::env::var("IRIS_ODSY_REC") { + Ok(path) => Some(record::Recorder::create(&path).map_err(|e| format!("IRIS_ODSY_REC {path}: {e}"))?), + Err(_) => None, + })) + } + + /// `mem`, recorded through `rec`. + fn recorded_mem(mem: Arc, rec: &record::RecHandle) -> Arc { + Arc::new(record::RecordingMem { inner: mem, rec: rec.clone() }) + } + + fn with_board( + mut s: Box, + small: u64, + large: u64, + status: Arc, + rec: record::RecHandle, + pool: (usize, u32), + raster: (crate::config::RasterUnit, &str), + ) -> Result, String> { // The fragment pipeline's workers and tile size (`[graphics]`). s.raster.set_pool(pool.0, pool.1); if raster.0 == crate::config::RasterUnit::Wgpu { @@ -157,24 +217,14 @@ impl Odyssey { #[cfg(not(feature = "wgpu"))] return Err("graphics.raster = \"wgpu\" needs a build with the wgpu feature".into()); } - // The recorder's slot is there whether or not a recording runs, so - // the monitor can start one (`vpro rec`); the DMA path records - // through it. - let rec = record::RecSlot::new(match std::env::var("IRIS_ODSY_REC") { - Ok(path) => Some(record::Recorder::create(&path).map_err(|e| format!("IRIS_ODSY_REC {path}: {e}"))?), - Err(_) => None, - }); - let mem: Arc = Arc::new(record::RecordingMem { inner: mem, rec: rec.clone() }); - s.guest = Some(mem); - s.front.lock().irq = Some(irq); if let Ok(path) = std::env::var("IRIS_ODSY_TRACE") { s.trace.start(&path, trace::TRACE_ALL).map_err(|e| format!("IRIS_ODSY_TRACE {path}: {e}"))?; } let mut state = BoardLocks::new(s); state.rec = rec; Ok(Arc::new(Self { - small: widget_small_base(widget), - large: widget_large_base(widget), + small, + large, state, display: Arc::new(Display::new(status)), running: Arc::new(AtomicBool::new(false)), diff --git a/src/dev/odyssey/pi_gfx.rs b/src/dev/odyssey/pi_gfx.rs new file mode 100644 index 000000000..cf1bb67a3 --- /dev/null +++ b/src/dev/odyssey/pi_gfx.rs @@ -0,0 +1,210 @@ +//! Graphics flow control in the Hub and Bedrock processor interfaces. The +//! Octane has no processor-interface link (its board's `pi` stays `None`), +//! so on that machine the credit calls are no-ops. `PiGfx` is the register +//! block a node's processor interface embeds; its functions are pure state. +//! The board reaches the link through the `PiGfxLink` trait. +//! +//! Ported from hw/mips/sgi/pi-gfx.c in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use std::cell::Cell; + +pub(super) const PI_GFX_SLOTS: usize = 2; +pub const PI_GFX_REGS: u32 = 10; + +/// The five registers of one slot, in address order. +pub(super) const PI_GFX_PAGE: u32 = 0; +pub(super) const PI_GFX_CREDIT: u32 = 1; +pub(super) const PI_GFX_BIAS: u32 = 2; +pub(super) const PI_GFX_INT_COUNT: u32 = 3; +pub(super) const PI_GFX_SLOT_REGS: u32 = 5; + +/// Page: bit 40 enables it, bits 39:17 name it; the rest is reserved. +pub(super) const PI_GFX_PAGE_ENABLE: u64 = 1 << 40; +pub(super) const PI_GFX_PAGE_ADDRESS: u64 = 0xfffffe0000; +pub(super) const PI_GFX_PAGE_MASK: u64 = PI_GFX_PAGE_ENABLE | PI_GFX_PAGE_ADDRESS; +pub(super) const PI_GFX_CREDIT_MASK: u32 = 0xfff; +pub(super) const PI_GFX_BIAS_MASK: u32 = 0xfff; +pub(super) const PI_GFX_INT_MASK: u32 = 0x3ffffff; + +#[derive(Clone, Copy, Default)] +pub struct PiGfx { + pub page: [u64; PI_GFX_SLOTS], + pub credit: [u32; PI_GFX_SLOTS], + pub bias: [u32; PI_GFX_SLOTS], + pub int_count: [u32; PI_GFX_SLOTS], + pub int_compare: [u32; PI_GFX_SLOTS], + /// The slot's graphics interrupt has been raised for the counter falling + /// below the bias, and not yet re-armed by the counter rising above it. + pub starved: [bool; PI_GFX_SLOTS], + /// Host-only: the zero-length stall has been logged for this slot. + pub stall_logged: [bool; PI_GFX_SLOTS], + /// Bedrock: a counted store left the counter below the bias, and the + /// slot's next counted store waits until the processor has taken the + /// graphics interrupt or the counter has risen above the bias. + pub stopped: [bool; PI_GFX_SLOTS], + /// Bedrock: the interrupt counter runs (the credit counter is at zero). + pub int_running: [bool; PI_GFX_SLOTS], + /// The clock tick at which int_count held its value. + pub int_epoch: [u64; PI_GFX_SLOTS], +} + +/// The interface a node device implements for the graphics heads behind it +/// (return_credits, owe_credits and repay_credits). `widget` is the head's crosstalk widget number, the +/// value the driver writes into the node-widget map. +pub(super) trait PiGfxLink: Send + Sync { + /// A graphics head took `n` counted stores off its port. + fn return_credits(&self, widget: u32, n: u32); + /// Credits a head owes for counted stores it has not yet taken off its + /// port; a negative count cancels owed credits, as a board reset that + /// abandons the words does. A node without a ledger ignores the debt. + fn owe_credits(&self, widget: u32, n: i32) { + let _ = (widget, n); + } + /// Owed credits returned as the head takes the words. A node without a + /// ledger routes a repayment as an ordinary return. + fn repay_credits(&self, widget: u32, n: u32) { + self.return_credits(widget, n); + } +} + +/// Return `credits` to `node` for stores a head took off its port. +pub(super) fn pi_gfx_return_credits(node: &dyn PiGfxLink, widget: u32, credits: u32) { + node.return_credits(widget, credits); +} + +/// Record `credits` a head owes `node` for stores it has not yet taken. +pub(super) fn pi_gfx_owe_credits(node: &dyn PiGfxLink, widget: u32, credits: i32) { + node.owe_credits(widget, credits); +} + +/// Repay `credits` that `node` held as owed. +pub(super) fn pi_gfx_repay_credits(node: &dyn PiGfxLink, widget: u32, credits: u32) { + node.repay_credits(widget, credits); +} + +impl PiGfx { + /// The counter above the bias re-arms the slot's graphics interrupt. + fn rearm(&mut self, slot: usize) { + if self.credit[slot] > self.bias[slot] { + self.starved[slot] = false; + } + } + + /// Register n of the block, 0 to PI_GFX_REGS - 1, counted in + /// doublewords from PI_GFX_BASE. + pub fn read(&self, n: u32) -> u64 { + let slot = (n / PI_GFX_SLOT_REGS) as usize; + + assert!(n < PI_GFX_REGS); + match n % PI_GFX_SLOT_REGS { + PI_GFX_PAGE => self.page[slot], + PI_GFX_CREDIT => self.credit[slot] as u64, + PI_GFX_BIAS => self.bias[slot] as u64, + PI_GFX_INT_COUNT => self.int_count[slot] as u64, + _ => self.int_compare[slot] as u64, + } + } + + /// A write keeps the field's bits; a write to the credit counter replaces + /// the count. + pub fn write(&mut self, n: u32, value: u64) { + let slot = (n / PI_GFX_SLOT_REGS) as usize; + + assert!(n < PI_GFX_REGS); + match n % PI_GFX_SLOT_REGS { + PI_GFX_PAGE => self.page[slot] = value & PI_GFX_PAGE_MASK, + PI_GFX_CREDIT => { + self.credit[slot] = (value as u32) & PI_GFX_CREDIT_MASK; + self.rearm(slot); + } + PI_GFX_BIAS => self.bias[slot] = (value as u32) & PI_GFX_BIAS_MASK, + PI_GFX_INT_COUNT => self.int_count[slot] = (value as u32) & PI_GFX_INT_MASK, + _ => self.int_compare[slot] = (value as u32) & PI_GFX_INT_MASK, + } + } + + /// Whether the slot's page is enabled and covers physical address `addr`. + pub fn covers(&self, slot: usize, addr: u64) -> bool { + assert!(slot < PI_GFX_SLOTS); + let page = self.page[slot]; + (page & PI_GFX_PAGE_ENABLE) != 0 + && (addr & PI_GFX_PAGE_ADDRESS) == (page & PI_GFX_PAGE_ADDRESS) + } + + /// An uncached I/O store by the processor in `slot` reached physical + /// address bits 39:0 `addr`. Charges one credit when the slot's page is + /// enabled and covers it, and returns whether it did. `who` names the + /// device in log messages. + pub fn count_store(&mut self, slot: usize, addr: u64, who: &str) -> bool { + if !self.covers(slot, addr) { + return false; + } + // Whether the twelve-bit counter wraps, saturates or goes negative is + // unknown. It wraps, and says so. + if self.credit[slot] == 0 { + crate::dlog!(LogModule::Odyssey, "{}: graphics credit counter {} wrapped below zero", who, (b'A' + slot as u8) as char); + } + self.credit[slot] = self.credit[slot].wrapping_sub(1) & PI_GFX_CREDIT_MASK; + true + } + + /// After a counted store: whether the slot's graphics interrupt must be + /// raised now. True once when the counter has fallen below the bias; the + /// counter rising above the bias again re-arms it. The stall that + /// accompanies it on the chip is modelled as zero length. + pub fn take_starved(&mut self, slot: usize, who: &str) -> bool { + assert!(slot < PI_GFX_SLOTS); + if self.starved[slot] || self.credit[slot] >= self.bias[slot] { + return false; + } + self.starved[slot] = true; + if !self.stall_logged[slot] { + self.stall_logged[slot] = true; + crate::dlog!( + LogModule::Odyssey, + "{}: graphics credit counter {} fell below its bias; interrupt raised at once, the write stall and the interrupt counter are not modelled", + who, + (b'A' + slot as u8) as char + ); + } + true + } + +} + +thread_local! { + /// The mark on the store a processor path is forwarding, per host + /// thread: the forward and the head's handler run on the issuing + /// vCPU's thread. + static PI_GFX_FORWARDING_COUNTED: Cell = const { Cell::new(false) }; +} + +/// A processor interface marks each store it forwards down its I/O path; +/// returns the previous mark, to be restored with pi_gfx_unmark_store. +pub(super) fn pi_gfx_mark_store(counted: bool) -> bool { + PI_GFX_FORWARDING_COUNTED.with(|m| m.replace(counted)) +} + +pub(super) fn pi_gfx_unmark_store(previous: bool) { + PI_GFX_FORWARDING_COUNTED.with(|m| m.set(previous)); +} + +/// The one-shot read: the call clears the mark. +pub(super) fn pi_gfx_take_mark() -> bool { + PI_GFX_FORWARDING_COUNTED.with(|m| m.replace(false)) +} + +impl PiGfx { + /// `credits` credits returned to `slot`. + pub fn add_credits(&mut self, slot: usize, credits: u32, who: &str) { + assert!(slot < PI_GFX_SLOTS); + let sum = self.credit[slot] as u64 + credits as u64; + if sum > PI_GFX_CREDIT_MASK as u64 { + crate::dlog!(LogModule::Odyssey, "{}: graphics credit counter {} wrapped above {:#x}", who, (b'A' + slot as u8) as char, PI_GFX_CREDIT_MASK); + } + self.credit[slot] = (sum & PI_GFX_CREDIT_MASK as u64) as u32; + self.rearm(slot); + } +} + diff --git a/src/dev/odyssey/tests/display.rs b/src/dev/odyssey/tests/display.rs index afa925719..af970aaa8 100644 --- a/src/dev/odyssey/tests/display.rs +++ b/src/dev/odyssey/tests/display.rs @@ -180,8 +180,8 @@ fn a_flip_waits_until_the_raster_unit_covers_the_stores_before_its_arm() { route_to_heart(&mut s); // Two stores queued in the command stream, not yet run. - assert!(s.fifo.push(&s.front, cmd::FIFO, (swap::SYNC_A as u64) << 32 | 0x8000_2000, 8)); - assert!(s.fifo.push(&s.front, cmd::FIFO, (swap::SYNC_B as u64) << 32 | 0x8000_2000, 8)); + assert!(s.fifo.push(&s.front, cmd::FIFO, (swap::SYNC_A as u64) << 32 | 0x8000_2000, 8, false)); + assert!(s.fifo.push(&s.front, cmd::FIFO, (swap::SYNC_B as u64) << 32 | 0x8000_2000, 8, false)); assert_eq!(s.fifo.pushed(), 2); assert_eq!(s.fifo.retired(), 0); diff --git a/src/dev/odyssey/tests/fifo.rs b/src/dev/odyssey/tests/fifo.rs index 8e90e12eb..0dd759e7d 100644 --- a/src/dev/odyssey/tests/fifo.rs +++ b/src/dev/odyssey/tests/fifo.rs @@ -1,4 +1,4 @@ -//! The command FIFO: queueing, the board's thread, status. +//! The command FIFO: queueing, the board's thread, status, credits. use crate::dev::odyssey::board::Board; use crate::dev::odyssey::command::{CFIFO_PREAMBLE, CfifoPreamble}; @@ -8,6 +8,10 @@ use crate::dev::odyssey::consts::{ use crate::dev::odyssey::fifo::{CommandFifo, RING_STORES, RUN_MAX, Run}; use crate::dev::odyssey::interface::HostInterface; use crate::dev::odyssey::mmio::BoardLocks; +use crate::dev::odyssey::pi_gfx::{ + pi_gfx_mark_store, pi_gfx_take_mark, pi_gfx_unmark_store, PI_GFX_BIAS, PI_GFX_CREDIT, + PI_GFX_PAGE, PI_GFX_PAGE_ENABLE, PiGfx, PiGfxLink, +}; use crate::dev::odyssey::tests::{ board, board_with_ram, cmd, cmd64, reg_read, reg_write, route_to_heart, select, FLAG_VECTOR, Ram, @@ -210,41 +214,151 @@ fn a_waiting_bus_read_is_busy_until_the_board_thread_has_run_the_stores() { h.work_stop(); } +/// Records every call a board makes on its processor-interface link. +#[derive(Default)] +struct PiStub(Mutex>); + +impl PiGfxLink for PiStub { + fn return_credits(&self, widget: u32, n: u32) { + self.0.lock().push(("return", widget, n as i64)); + } + fn owe_credits(&self, widget: u32, n: i32) { + self.0.lock().push(("owe", widget, n as i64)); + } + fn repay_credits(&self, widget: u32, n: u32) { + self.0.lock().push(("repay", widget, n as i64)); + } +} + +impl PiStub { + fn count(&self, kind: &str) -> i64 { + self.0.lock().iter().filter(|c| c.0 == kind).map(|c| c.2).sum() + } + /// Credits given back so far, returned or repaid. + fn settled(&self) -> i64 { + self.count("return") + self.count("repay") + } +} + +/// A board whose stores are all counted and queued, with a stub link. +fn credit_board() -> (Box, Arc) { + let (s, _) = board("v12"); + let pi = Arc::new(PiStub::default()); + { + let mut f = s.front.lock(); + f.pi = Some(pi.clone()); + f.test_counted_stores = 1; + } + // No work thread runs: hold the queue and step by hand. + s.fifo.test_words.store(1, SeqCst); + (s, pi) +} + +/// Command work credits at FIFO entry: a counted store's credit +/// comes back when the board takes it into its FIFO, the oldest +/// `cfifo::DEPTH` waiting stores at once; each store behind them is owed, then +/// repaid as the work ahead runs and it moves up. A store that does +/// not wait is credited inside the store. +/// +/// QEMU: command-work-credits-at-fifo-entry. +#[test] +fn a_counted_store_is_credited_as_it_enters_the_fifo() { + let (mut s, pi) = credit_board(); + let stores = cfifo::DEPTH + 8; + + for _ in 0..stores { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + // Credits count stores; the level counts words, two a doubleword, and + // reads no more than the model's FIFO size. + assert_eq!(s.fifo.stores(), stores); + assert_eq!(s.fifo.level(), (2 * stores).min(cfifo::WORDS)); + assert_eq!(s.fifo.in_fifo(), cfifo::DEPTH); + assert_eq!(pi.count("return"), cfifo::DEPTH as i64); + assert_eq!(pi.count("owe"), (stores - cfifo::DEPTH) as i64); + assert_eq!(pi.count("repay"), 0); + assert!(pi.0.lock().iter().all(|c| c.1 == VPRO_WIDGET)); + + // One store runs per slice: the next one moves up and is repaid. + s.work_step(&mut Budget::stores(1)); + assert_eq!(pi.count("repay"), 1); + assert_eq!(pi.settled(), cfifo::DEPTH as i64 + 1); + + s.work_step(&mut Budget::unlimited()); + assert!(!s.fifo.pending()); + assert_eq!(pi.settled(), stores as i64); + + // A store that does not wait is credited inside the store. + s.fifo.test_words.store(0, SeqCst); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert!(!s.work_pending()); + assert_eq!(pi.settled(), stores as i64 + 1); + assert_eq!(pi.count("owe"), (stores - cfifo::DEPTH) as i64); +} + /// A run of stores the board's thread has looked at and then runs leaves -/// the FIFO together. The stores run in order; the level and the store count -/// end where taking the stores out one by one leaves them, including when -/// more stores join the queue after the run was taken. +/// the FIFO together. The stores run in order; the level, the store count +/// and the credits end where taking the stores out one by one leaves them, +/// including when a counted store joins the queue after the run was taken. #[test] -fn a_run_retires_with_the_levels_of_one_by_one() { +fn a_run_retires_with_the_levels_and_credits_of_one_by_one() { fn queue(s: &mut Board) { - for _ in 0..70 { + s.front.lock().test_counted_stores = 0; + for _ in 0..cfifo::DEPTH + 6 { s.mmio_write(consts::cmd::FIFO, 0, 8); } } - fn join(s: &mut Board) { + fn counted_join(s: &mut Board) { + s.front.lock().test_counted_stores = 1; for _ in 0..10 { s.mmio_write(consts::cmd::FIFO, 0, 8); } } - // One at a time: the store count and the level are the reference. - let (mut one, _, _) = queued_board(); + // One at a time: the store count and the credits are the reference. + let (mut one, pi_one) = credit_board(); queue(&mut one); - join(&mut one); + counted_join(&mut one); for _ in 0..RUN_MAX { step(&mut one); } - let (mut run, _, _) = queued_board(); + let (mut run, pi_run) = credit_board(); queue(&mut run); let mut taken = Run::new(); run.fifo.take_run(&mut taken); - assert_eq!(taken.stores().len(), RUN_MAX, "a full run"); - join(&mut run); - assert_eq!(run.fifo.stores(), 80); + assert_eq!(taken.stores().len(), RUN_MAX, "no counted store waits yet: a full run"); + counted_join(&mut run); + assert_eq!(run.fifo.stores(), cfifo::DEPTH + 16); run.fifo.retire(&run.front, &taken, RUN_MAX); assert_eq!(run.fifo.stores(), one.fifo.stores()); assert_eq!(run.fifo.level(), one.fifo.level()); + assert_eq!(pi_run.count("repay"), pi_one.count("repay")); + assert_eq!(pi_run.count("return"), pi_one.count("return")); + assert_eq!(pi_run.count("owe"), pi_one.count("owe")); + assert!(pi_run.count("repay") > 0, "the run moved owed stores up"); + // The counted stores left all sit inside the credited depth, so the + // next run is full. + assert!(run.fifo.stores() <= cfifo::DEPTH); + run.fifo.take_run(&mut taken); + assert_eq!(taken.stores().len(), RUN_MAX); +} + +/// The board's thread takes stores one at a time only while some wait past +/// the credited depth, where one may owe its credit; a backlog of counted +/// stores inside the depth, all credited, goes in full runs. +#[test] +fn a_run_is_one_store_only_past_the_credited_depth() { + let (mut s, _) = credit_board(); + for _ in 0..cfifo::DEPTH { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + let mut run = Run::new(); + s.fifo.take_run(&mut run); + assert_eq!(run.stores().len(), RUN_MAX, "every waiting store is credited"); + s.mmio_write(consts::cmd::FIFO, 0, 8); + s.fifo.take_run(&mut run); + assert_eq!(run.stores().len(), 1, "one store owes its credit"); } /// A slice cut short by its budget retires only the stores it ran: the rest @@ -265,6 +379,94 @@ fn a_slice_cut_short_leaves_the_rest_of_its_run_queued() { assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5); } +/// A reset abandons the waiting stores and cancels each owed credit +/// (one owe of -1 per owed store); stores not owed cancel nothing. +#[test] +fn a_reset_cancels_the_credits_owed_for_abandoned_stores() { + let (mut s, pi) = credit_board(); + + for _ in 0..cfifo::DEPTH + 8 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(pi.count("owe"), 8); + s.work_reset(); + assert!(!s.fifo.pending()); + assert_eq!(s.fifo.in_fifo(), 0); + assert_eq!(pi.count("owe"), 0); + assert_eq!(pi.0.lock().iter().filter(|c| c.0 == "owe" && c.2 == -1).count(), 8); + assert_eq!(pi.count("repay"), 0); +} + +/// The processor interface's mark makes a store counted, once, and a +/// counted store with no link logs and returns nothing. +#[test] +fn the_processor_mark_counts_exactly_one_store() { + let (mut s, pi) = credit_board(); + + { + let mut f = s.front.lock(); + f.test_counted_stores = 0; + } + s.fifo.test_words.store(0, SeqCst); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(pi.settled(), 0); + + let previous = pi_gfx_mark_store(true); + s.mmio_write(consts::cmd::FIFO, 0, 8); + pi_gfx_unmark_store(previous); + assert_eq!(pi.0.lock().as_slice(), &[("return", VPRO_WIDGET, 1)]); + + // One-shot: the mark was taken by the first store. + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(pi.settled(), 1); + + // The Octane: no link, so the credit is dropped with a log line. + s.front.lock().pi = None; + pi_gfx_mark_store(true); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(pi.settled(), 1); + assert!(!pi_gfx_take_mark()); +} + +/// A counted store the bus path takes while the board's thread is stopped +/// runs on the processor, and its credit still comes back: the mark is +/// taken once and handed to the store that runs. +#[test] +fn a_counted_bus_store_returns_its_credit_with_the_board_thread_stopped() { + let (s, _) = board("v12"); + let pi = Arc::new(PiStub::default()); + s.front.lock().pi = Some(pi.clone()); + let h = BoardLocks::new(s); + assert!(!h.fifo.running.load(SeqCst)); + + let previous = pi_gfx_mark_store(true); + assert!(h.bus_write(consts::cmd::FIFO, 0, 8)); + pi_gfx_unmark_store(previous); + assert_eq!(pi.0.lock().as_slice(), &[("return", VPRO_WIDGET, 1)]); + assert!(!pi_gfx_take_mark(), "the mark was spent by that store"); +} + +/// The register block's pure state (PiGfx). +#[test] +fn the_credit_block_starves_a_widget_that_runs_out_of_credits() { + let mut g = PiGfx::default(); + + g.write(PI_GFX_PAGE, PI_GFX_PAGE_ENABLE | 0x2_0000); + g.write(PI_GFX_CREDIT, 3); + g.write(PI_GFX_BIAS, 2); + assert!(g.covers(0, 0x2_1000)); + assert!(!g.covers(0, 0x4_0000)); + assert!(g.count_store(0, 0x2_0000, "t")); + assert!(!g.take_starved(0, "t")); + assert!(g.count_store(0, 0x2_0000, "t")); + assert!(g.take_starved(0, "t")); + assert!(!g.take_starved(0, "t")); + g.add_credits(0, 5, "t"); + assert_eq!(g.read(PI_GFX_CREDIT), 6); + assert!(!g.starved[0]); +} + + /// The model's low-water threshold, in waiting words (a model choice). const LOW_WATER_MARK: u32 = cfifo::LOW_WATER; const HOST_WRITE_BASE: u64 = 0xb70000; @@ -508,6 +710,8 @@ fn the_threshold_follows_6b2() { /// MODEL CONTRACT ONLY: the FIFO's size is unknown and the model's size is /// its own choice. Whatever `0x6b1` the set-up stores, the level clamps at /// cfifo::WORDS however many stores wait. +/// +/// QEMU: command-work-credit-depth-ignores-setup covers the credit depth. #[test] fn the_fifo_size_ignores_the_set_up_words() { for word_6b1 in [1u32, 256, 0x7fff_ffff] { @@ -555,35 +759,129 @@ fn low_water_is_raised_as_the_queue_falls_below_the_mark() { assert_eq!(rec.events(FLAG_VECTOR), [true]); } -/// A hurry ends the board thread's pause before a slice. The pause is set -/// far longer than the wait below, so only the hurry runs the stores in -/// time; the stores wait while the thread sits in it. +/// MODEL CONTRACT ONLY: the FIFO's depth is unknown and the model's depth is +/// its own choice. The stored `0x6b1` and `0x6b2` values (256 and 32192, as +/// the kernels write them) do not widen the credits: only the oldest +/// `cfifo::DEPTH` waiting stores, the model's depth, have their credits back, so the +/// processor interface still holds back a writer that runs ahead of the slow +/// board. +/// +/// QEMU: command-work-credit-depth-ignores-setup. #[test] -fn a_hurry_ends_the_board_threads_pause() { - let (s, _rec) = board("v12"); +fn the_fifo_set_up_does_not_widen_the_credit_depth() { + let (mut s, pi) = credit_board(); + s.fifo.test_words.store(0, SeqCst); + cmd64(&mut s, 0x6b1, 256); + cmd64(&mut s, 0x6b3, 24144); + cmd64(&mut s, 0x6b2, 32192); + finish(&mut s); + pi.0.lock().clear(); + + s.fifo.test_words.store(1, SeqCst); + let stores = cfifo::DEPTH + 8; + for _ in 0..stores { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(s.fifo.stores(), stores); + assert_eq!(pi.count("return"), cfifo::DEPTH as i64); + finish(&mut s); + assert_eq!(pi.settled(), stores as i64); +} + +/// MODEL CHOICE: the FIFO holds far more than the host's credit window, so +/// a writer that fills the whole window, 256 doublewords on the IP30 and at most 256 on +/// the IP35, gets every credit back as its stores enter, with the board's +/// thread taking none of them, and owes nothing. A credit that waited on +/// the board's thread could miss the high-water handler's ten-microsecond +/// poll. +#[test] +fn the_credit_depth_is_deeper_than_the_host_window() { + const LARGEST_HOST_WINDOW: u32 = 256; + let (mut s, pi) = credit_board(); + for _ in 0..=LARGEST_HOST_WINDOW { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(s.fifo.stores(), LARGEST_HOST_WINDOW + 1, "the board took none"); + assert_eq!(pi.count("return"), LARGEST_HOST_WINDOW as i64 + 1); + assert_eq!(pi.count("owe"), 0); + finish(&mut s); +} + +/// MODEL CHOICE: the credited depth is 16,384 stores, the smallest power of +/// two that holds the kernel's start-up program of about 12,291 words at +/// one word per store. With the board taking nothing, every one of the +/// first 16,384 counted stores is credited as it enters, and +/// the next is owed until the board takes a store. +#[test] +fn every_store_inside_the_credited_depth_is_credited_and_the_next_is_owed() { + const DEPTH: u32 = 16_384; + let (mut s, pi) = credit_board(); + for _ in 0..DEPTH { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(s.fifo.stores(), DEPTH, "the board took none"); + assert_eq!(pi.count("return"), DEPTH as i64); + assert_eq!(pi.count("owe"), 0); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(pi.count("return"), DEPTH as i64); + assert_eq!(pi.count("owe"), 1, "the store past the depth owes its credit"); + finish(&mut s); + assert_eq!(pi.settled(), DEPTH as i64 + 1); +} + +/// A counted store whose credit is owed has the board's thread run without +/// its pause, so the credit comes back as soon as the stores ahead retire. +/// A guest that watches its credit count reads a register the bus does not +/// refuse, so nothing else hurries the thread; with the pause the kernel saw +/// its count fall below the bias in a burst of a few hundred stores and gave +/// the FIFO up as stuck. The stores inside the model's depth are credited +/// at once and do not hurry. +#[test] +fn a_counted_store_owed_its_credit_hurries_the_board_thread() { + let (mut s, pi) = credit_board(); + for _ in 0..cfifo::DEPTH { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert!(!s.fifo.take_hurry(), "a credited store leaves the pause alone"); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(pi.count("owe"), 1); + assert!(s.fifo.take_hurry(), "an owed credit hurries the board's thread"); + finish(&mut s); + + // On the board's thread, with a pause far longer than the wait below: + // only the hurry runs the burst in time. + let stores = cfifo::DEPTH + 136; let h = BoardLocks::new(s); h.fifo.test_gap_ms.store(60_000, SeqCst); + // credit_board holds the queue for stepping by hand. + h.fifo.test_words.store(0, SeqCst); + pi.0.lock().clear(); h.work_start(); - // One store starts the thread, and the burst waits until the thread is - // in its pause: a burst that lands before it would find the request - // already made and prove nothing about waking a sleeper. + // One credited store starts the thread, and the burst waits until the + // thread is in its pause: a burst that lands before it would find the + // request already made and prove nothing about waking a sleeper. h.write(consts::cmd::FIFO, 0, 8); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); while h.fifo.test_parked.load(SeqCst) == 0 { assert!(std::time::Instant::now() < deadline, "the board's thread never reached its pause"); std::thread::yield_now(); } - for _ in 1..200 { + for _ in 1..stores { h.write(consts::cmd::FIFO, 0, 8); } - assert_eq!(h.fifo.stores(), 200, "the stores wait out the pause"); - h.fifo.hurry(); + // The last few stores may find the queue run dry and wait out a pause + // of their own, but inside the depth they are credited at the store. let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); - while h.fifo.stores() != 0 { - assert!(std::time::Instant::now() < deadline, "the board's thread kept its pause: {} stores wait", h.fifo.stores()); + while pi.settled() != stores as i64 { + assert!(std::time::Instant::now() < deadline, "the board's thread kept its pause: {} stores wait, {} of {stores} credits back", h.fifo.stores(), pi.settled()); std::thread::yield_now(); } + assert!(pi.count("owe") > 0, "the burst ran past the model's depth"); + assert_eq!(pi.count("repay"), pi.count("owe")); + // Cut a pause still under way short, so the stop does not wait it out. + h.fifo.hurry(); h.work_stop(); + assert_eq!(h.fifo.stores(), 0); } /// A stopping machine finishes the work waiting; a reset abandons it. @@ -628,7 +926,7 @@ fn two_processors_storing_to_the_fifo_lose_nothing_and_keep_each_ones_order() { let pushed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { for seq in 0..PER { let size = if seq % 3 == 0 { 8 } else { 4 }; - while !h.fifo.push(&h.front, consts::cmd::FIFO, id << 32 | seq, size) { + while !h.fifo.push(&h.front, consts::cmd::FIFO, id << 32 | seq, size, false) { std::thread::yield_now(); } } @@ -676,7 +974,7 @@ fn a_store_run_before_its_push_returns_is_already_in_the_level() { let h = BoardLocks::new(s); h.front.lock().intr_flags &= !intr::FIFO_LOW_WATER; for seq in 0..20 { - assert!(h.fifo.push(&h.front, consts::cmd::FIFO, seq, 4)); + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, seq, 4, false)); } fn run_everything(fifo: &CommandFifo, front: &Mutex) { let mut run = Run::new(); @@ -687,7 +985,7 @@ fn a_store_run_before_its_push_returns_is_already_in_the_level() { assert_eq!((fifo.stores(), fifo.level()), (0, 0), "the run took out what the level counted"); } *h.fifo.test_after_ring_push.lock() = Some(run_everything); - assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 20, 8)); + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 20, 8, false)); assert!(h.fifo.test_after_ring_push.lock().is_none(), "the consumer ran inside the push"); assert_eq!((h.fifo.stores(), h.fifo.level()), (0, 0)); assert_ne!(h.front.lock().intr_flags & intr::FIFO_LOW_WATER, 0, "the level fell through the mark"); @@ -819,7 +1117,7 @@ fn two_processors_racing_for_the_last_place_in_the_ring_leave_one_store_in() { // The ring has room for one more store when it holds RING_STORES - 1. let fill = |h: &BoardLocks| { for _ in 0..RING_STORES - 1 { - assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4)); + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4, false)); } }; fill(&h); @@ -834,7 +1132,7 @@ fn two_processors_racing_for_the_last_place_in_the_ring_leave_one_store_in() { for _ in 0..ROUNDS { start.wait(); // A panic in a push must fail the round, not leave the others waiting. - let pushed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4))); + let pushed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4, false))); match pushed { Ok(true) => { accepted.fetch_add(1, SeqCst); @@ -904,7 +1202,7 @@ fn processors_wait_for_each_other_and_the_board_thread_does_not_wait_for_them() // before the lock was held. Nothing is queued here, so queue through the // unlocked ring and see it drain. for w in &words[..4] { - assert!(h.fifo.push(&h.front, w.port, w.value, w.size as u32)); + assert!(h.fifo.push(&h.front, w.port, w.value, w.size as u32, false)); } let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); while h.fifo.pending() { @@ -951,13 +1249,14 @@ fn a_refused_command_push_retries_behind_the_board_thread() { let (h, _rec) = threaded_board(); let words = preamble_twice(); - // The ring must fill and the retry be seen waiting while this one slice - // holds the board, however long the test's threads wait for a core. - start_long_slice(&h, &words, 4000); + // The slice holds the board's thread until the test releases it, so the + // ring stays full however long the host takes to get here. + start_long_slice(&h, &words, 600_000); while h.fifo.stores() < RING_STORES { assert!(h.bus_write(consts::cmd::FIFO, 0, 4)); } let stores = h.fifo.stores(); + let refused = h.fifo.test_refused.load(SeqCst); let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); let store = { let (h, done) = (h.clone(), done.clone()); @@ -966,9 +1265,15 @@ fn a_refused_command_push_retries_behind_the_board_thread() { done.store(true, SeqCst); }) }; - std::thread::sleep(std::time::Duration::from_millis(150)); + // The store is refused and retries: wait until it has been. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while h.fifo.test_refused.load(SeqCst) == refused { + assert!(std::time::Instant::now() < deadline, "the store was never refused"); + std::thread::yield_now(); + } assert!(!done.load(SeqCst), "the store was dropped or ran around the board's thread"); assert_eq!(h.fifo.stores(), stores); + h.fifo.test_release.store(true, SeqCst); store.join().unwrap(); assert!(h.fifo.level() <= cfifo::WORDS); h.fifo.test_words.store(0, SeqCst); diff --git a/src/dev/odyssey/tests/interrupts.rs b/src/dev/odyssey/tests/interrupts.rs index 6bbb67ca9..8f89604cf 100644 --- a/src/dev/odyssey/tests/interrupts.rs +++ b/src/dev/odyssey/tests/interrupts.rs @@ -437,3 +437,26 @@ fn a_held_flag_is_sent_with_the_source_enables_clear() { reg_write(&mut s, route::DEST_UPPER, HEART_UPPER); assert_eq!(rec.events(FLAG_VECTOR), [true]); } + +/// A board built for a machine whose fabric carries packets to any widget +/// hands them to that machine's sender, to the widget the routing names, +/// and never to HEART's line: the Fuel's widget 13 reaches Bedrock this way. +#[test] +fn a_packet_sender_takes_the_interrupt_to_the_routed_widget() { + let (mut s, rec) = board("v12"); + let seen = Arc::new(Mutex::new(Vec::new())); + let log = seen.clone(); + s.front.lock().packets = Some(Arc::new(move |widget, address, vector, level| { + log.lock().push((widget, address, vector, level)); + true + })); + s.mmio_write(ROUTE_DEST_UPPER, 1 << 25, 4); + s.mmio_write(ROUTE_DEST_LOWER, 0x1230, 4); + s.mmio_write(ROUTE_VECTORS, TEST_VECTOR as u64, 4); + s.mmio_write(INTR_ENABLE_SET, INTR_FIFO_LOW_WATER, 4); + assert_eq!(*seen.lock(), vec![(1, 0x1230, TEST_VECTOR as u8, true)]); + assert!(rec.silent(), "the packet did not go to HEART"); + + s.mmio_write(INTR_CLEAR, INTR_FIFO_LOW_WATER, 4); + assert_eq!(seen.lock().last(), Some(&(1, 0x1230, TEST_VECTOR as u8, false))); +} diff --git a/src/dev/odyssey/worker.rs b/src/dev/odyssey/worker.rs index b0a44abf0..58fa0c728 100644 --- a/src/dev/odyssey/worker.rs +++ b/src/dev/odyssey/worker.rs @@ -23,7 +23,8 @@ pub(super) const ODYSSEY_WORK_THREAD: &str = "odyssey-work"; /// The pause before the board's thread runs a burst, so the stores of the /// burst land first and one run takes them together. A processor that /// waits on the thread cuts it short (`CommandFifo::hurry`): an access the -/// bus refused. +/// bus refused, or a counted store whose credit stays owed until the stores +/// ahead of it retire. pub(super) const GAP: Duration = Duration::from_micros(100); /// How much one run may do: for tests a count of stores. The budget is @@ -141,7 +142,11 @@ impl Board { // A run that takes as long as a long draw would. let sleep_ms = std::mem::take(&mut self.test_slice_sleep_ms); if sleep_ms != 0 { - std::thread::sleep(std::time::Duration::from_millis(sleep_ms as u64)); + // Until the time is up or a test releases the hold. + let end = std::time::Instant::now() + std::time::Duration::from_millis(sleep_ms as u64); + while std::time::Instant::now() < end && !self.fifo.test_release.load(SeqCst) { + std::thread::sleep(std::time::Duration::from_millis(1)); + } } } @@ -196,6 +201,6 @@ impl Board { /// Reset clears the FIFO and abandons the waiting stores. pub fn work_reset(&mut self) { - self.fifo.clear(); + self.fifo.clear(&self.front); } } From e3a1227dc6ea0b6230c513eb546ea6f9581ad9b5 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 04:22:15 -0700 Subject: [PATCH 19/20] ip35: fit the VPro board in the Fuel's graphics slot Every Fuel has a VPro graphics slot: a Fuel IRIX inventory lists its board under xtalk/13 and OpenBSD finds Odyssey at crossbar widget 13. Build the board there when the machine is given a V12 or a V10, through the widget-offset constructor, and keep none, the serial machine, as the default. The XBridge now reports link 13 alive, the board's DMA enters Bedrock's inbound path as widget 13, and a link reset of port 13 resets it. Its interrupt packets cross the crossbar to Bedrock like the PCI heads'. The status bar's activity (SCSI, IOC3 and Ethernet) and the CPU's Compare ticks reach its LEDs as on the Octane. IRIX's VPro driver writes each processor interface's graphics flow-control registers at attach and tears the board down when the credit counter reads zero, so the interfaces keep those registers; they count no stores and return no credits, so no processor interface is linked to the board's credit accounting. With PROM 6.210 the board passes the graphics diagnostic, and IRIX 6.5.30 with the VPro driver lists Graphics board: V12 and draws the X login screen on it. Input comes from the PS/2 pair the Fuel already has. Configuration accepts the board on the Fuel too, see the config note below. Tests build a Fuel board with each board and check that the processor reaches the board at widget 13 and finds its link alive, that its interrupt packets set and clear a processor interface's pending bit, that a link reset of its port resets it, that a headless Fuel has nothing there, and that the processor interfaces keep their graphics registers. Config (config): A Fuel is reported with both the V12 and the V10, so take either with [graphics] board; the other boards stay refused, with a message that names the Fuel's choices, and none, the serial machine, stays the default. The doc comments for the two boards now say they fit the Fuel as well as the Octane. The Fuel's validation case now accepts both boards and refuses the V6. --- src/config.rs | 28 ++++--- src/ip35/tests.rs | 138 ++++++++++++++++++++++++++++++++++ src/ip35_machine.rs | 179 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 322 insertions(+), 23 deletions(-) diff --git a/src/config.rs b/src/config.rs index fdce2fc73..8f5b8ba62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -819,14 +819,14 @@ pub enum GraphicsBoard { /// VPro V8: BUZZ A, 128 MB. Octane only. #[clap(name = "v8", alias = "vpro:v8", alias = "vpro_v8")] V8, - /// VPro V10: BUZZ B, 32 MB. Octane only. It shipped only in the Octane2 - /// and needs Crossbow 1.4, so the machine takes the Octane2's Crossbow and - /// Bridge revisions. + /// VPro V10: BUZZ B, 32 MB. Octane and Fuel. On the Octane it shipped + /// only in the Octane2 and needs Crossbow 1.4, so the machine takes the + /// Octane2's Crossbow and Bridge revisions. #[clap(name = "v10", alias = "vpro:v10", alias = "vpro_v10")] V10, - /// VPro V12: BUZZ B, 128 MB. Octane only. It shipped only in the Octane2 - /// and needs Crossbow 1.4, so the machine takes the Octane2's Crossbow and - /// Bridge revisions. + /// VPro V12: BUZZ B, 128 MB. Octane and Fuel. On the Octane it shipped + /// only in the Octane2 and needs Crossbow 1.4, so the machine takes the + /// Octane2's Crossbow and Bridge revisions. #[clap(name = "v12", alias = "vpro:v12", alias = "vpro_v12")] V12, /// No graphics board: the machine runs on its serial console. Octane @@ -892,7 +892,7 @@ impl GraphicsBoard { pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, - MachineProfile::FuelIp34 => self == Self::None, + MachineProfile::FuelIp34 => matches!(self, Self::V10 | Self::V12 | Self::None), MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } @@ -2482,6 +2482,8 @@ impl MachineConfig { if !board.supports(self.machine.profile) { return Err(if self.machine.profile == MachineProfile::OctaneIp30 { format!("graphics.board \"{name}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)") + } else if self.machine.profile == MachineProfile::FuelIp34 { + format!("graphics.board \"{name}\" is not a Fuel board (valid: v12, v10, none)") } else { format!("graphics.board \"{name}\" is only valid on the Octane") }); @@ -2564,8 +2566,8 @@ impl MachineConfig { if self.banks[..2] != [256, 256] { return Err("Fuel currently requires banks = [256, 256] (512 MiB)".into()); } - if board != GraphicsBoard::None { - return Err("Fuel currently supports graphics.board = \"none\" only".into()); + if !board.supports(MachineProfile::FuelIp34) { + return Err(format!("graphics.board \"{}\" is not a Fuel board (valid: v12, v10, none)", board.as_str())); } if let Some(text) = &self.fuel.l1_revision { crate::dev::l1::parse_revision(text).map_err(|e| format!("fuel.l1_revision: {e}"))?; @@ -4232,7 +4234,7 @@ mod fuel_input_tests { } #[test] - fn fuel_requires_its_fixed_memory_population_and_no_graphics() { + fn fuel_requires_its_fixed_memory_population_and_a_vpro_or_no_graphics() { let mut cfg = MachineConfig::default(); cfg.machine.profile = MachineProfile::FuelIp34; cfg.banks.fill(0); @@ -4246,6 +4248,12 @@ mod fuel_input_tests { cfg.validate().unwrap(); cfg.graphics.board = Some(GraphicsBoard::Newport); assert!(cfg.validate().unwrap_err().contains("none")); + for board in [GraphicsBoard::V12, GraphicsBoard::V10] { + cfg.graphics.board = Some(board); + cfg.validate().unwrap(); + } + cfg.graphics.board = Some(GraphicsBoard::V6); + assert!(cfg.validate().unwrap_err().contains("not a Fuel board")); cfg.graphics.board = None; cfg.banks[1] = 0; assert!(cfg.validate().unwrap_err().contains("256, 256")); diff --git a/src/ip35/tests.rs b/src/ip35/tests.rs index ef54b4918..865d0357c 100644 --- a/src/ip35/tests.rs +++ b/src/ip35/tests.rs @@ -240,6 +240,9 @@ fn the_configured_station_address_reaches_the_l1_and_the_ethernet() { timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), ps2_input: true, usb_input: true, l1: Default::default(), + graphics: crate::config::GraphicsBoard::None, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }).unwrap(); assert_eq!(board.l1.station_mac(), mac); assert_eq!(board.eth.mac(), mac); @@ -255,6 +258,9 @@ fn the_l1_identity_overrides_reach_the_controller() { station_mac: [8, 0, 0x69, 0x12, 0x34, 0x56], timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), ps2_input: true, usb_input: false, l1, + graphics: crate::config::GraphicsBoard::None, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }).unwrap().l1.config(); let stock = build(L1Identity::default()); assert_eq!(stock.revision, Some([1, 48, 1])); @@ -622,6 +628,9 @@ fn the_fuel_plugs_a_usb_keyboard_and_mouse_only_when_configured_on() { timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), ps2_input: true, usb_input, l1: Default::default(), + graphics: crate::config::GraphicsBoard::None, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }).unwrap(); // Power each port: one with a device shows it connected. board.usb.bar_write(0, 0x54, 0x100, 4); @@ -890,3 +899,132 @@ mod code_views { }); } } + +fn fuel_with_graphics(graphics: crate::config::GraphicsBoard) -> crate::ip35_machine::FuelBoard { + crate::ip35_machine::FuelBoard::with_config(crate::ip35_machine::FuelBoardConfig { + payload: &[], clock: Arc::new(|| 0), flash_path: None, + station_mac: [0x08, 0x00, 0x69, 0x21, 0x43, 0x65], + timekeeper: Arc::new(crate::dev::timekeeper::Timekeeper::fuel().unwrap()), + ps2_input: true, usb_input: true, + l1: Default::default(), + graphics, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), + }).unwrap() +} + +/// The VPro's link status in the crossbar, in its slot's data half. +const VPRO_LINK_STATUS: u64 = 0x100 + 5 * 0x40 + 0x10 + 4; +/// A processor access to widget 13 through Bedrock's small window. +const VPRO_PIO: u64 = 13 << 24; + +// The VPro cases follow tests/qtest/sn1-fuel-vpro-test.c in +// jamesbraid/qemu-sgi-origami: widget-identity, profile-identity-and-routing +// and headless-fuel-has-no-board. + +// VPro host-interface registers the tests below touch, as byte offsets in +// the board's widget space. The board's own constants are private to it. +const ODSY_IDENTITY: u64 = 0x00; +const ODSY_CONTROL: u64 = 0x20; +const ODSY_CONTROL_WIDGET_MASK: u32 = 0xf; +const ODSY_INT_DEST_LOWER: u64 = 0x38; +const ODSY_INTR_CLEAR: u64 = 0x101c; +const ODSY_INTR_ENABLE_SET: u64 = 0x1034; +const ODSY_INTR_FIFO_LOW_WATER: u32 = 0x200; +const ODSY_ROUTE_DEST_UPPER: u64 = 0x1044; +const ODSY_ROUTE_DEST_LOWER: u64 = 0x104c; +const ODSY_ROUTE_VECTORS: u64 = 0x1054; +const ODSY_ROUTE_DEST_WIDGET_SHIFT: u32 = 25; + +/// A Fuel IRIX inventory lists its VPro under xtalk/13: the processor must +/// reach the board there, learn its widget number from it, and find the +/// link alive in the crossbar. +#[test] +fn the_vpro_answers_at_widget_13_with_its_link_alive() { + for (graphics, revision) in [(crate::config::GraphicsBoard::V12, 0xa), (crate::config::GraphicsBoard::V10, 0xa)] { + let board = fuel_with_graphics(graphics); + let identity = board.io.pio_read(VPRO_PIO | (ODSY_IDENTITY + 4), 4); + assert_eq!(identity.status, BUS_OK); + assert_eq!(identity.data as u32, (revision << 28) | 0x0c01_3047, "{graphics:?}"); + let control = board.io.pio_read(VPRO_PIO | (ODSY_CONTROL + 4), 4).data as u32; + assert_eq!(control & ODSY_CONTROL_WIDGET_MASK, 13); + assert_ne!(board.xbridge.crossbar().read32(VPRO_LINK_STATUS).data & (1 << 31), 0); + } +} + +/// The monitor's `vpro` commands resolve on a Fuel that has a VPro, and are +/// unknown on one that has none. +#[test] +fn the_monitor_resolves_vpro_commands_on_a_fuel_with_a_vpro() { + let resolves = |board: &crate::ip35_machine::FuelBoard, command: &str| { + board.monitor_devices().iter().any(|d| d.register_commands().iter().any(|(c, _)| c == command)) + }; + let with = fuel_with_graphics(crate::config::GraphicsBoard::V12); + assert!(resolves(&with, "vpro"), "a Fuel with a V12 answers `vpro`"); + assert!(resolves(&with, "ps2"), "the other board commands stay"); + let without = fuel_with_graphics(crate::config::GraphicsBoard::None); + assert!(!resolves(&without, "vpro"), "a Fuel with no VPro has no `vpro`"); + assert!(resolves(&without, "ps2")); +} + +/// The board's interrupt packets cross the XBridge to Bedrock as the PCI +/// heads' do: a raised source sets its vector's pending bit in the +/// processor interface the destination names, and a cleared one clears it. +#[test] +fn vpro_interrupt_packets_set_and_clear_a_processor_interface_pending_bit() { + let board = fuel_with_graphics(crate::config::GraphicsBoard::V12); + // Register space (bit 47), the node's hub window and PI0's pending + // modify register, at the Bedrock's crossbar port 0xa. + let packet = (1u64 << 47) | 0x180_0000 | 0x90; + let vector = 5; + let write = |offset: u64, value: u64| { + assert_eq!(board.io.pio_write(VPRO_PIO | offset, 4, value), BUS_OK, "{offset:#x}"); + }; + write(ODSY_ROUTE_DEST_UPPER, (0xa << ODSY_ROUTE_DEST_WIDGET_SHIFT) | (packet >> 32)); + write(ODSY_ROUTE_DEST_LOWER, packet & 0xffff_ffff); + write(ODSY_ROUTE_VECTORS, vector); + write(ODSY_INTR_ENABLE_SET, ODSY_INTR_FIFO_LOW_WATER as u64); + let pending = || board.pi[0].read64(0x98).data & (1 << vector); + assert_ne!(pending(), 0); + write(ODSY_INTR_CLEAR, ODSY_INTR_FIFO_LOW_WATER as u64); + assert_eq!(pending(), 0); +} + +/// A link reset of the graphics port resets the board behind it, as the +/// PCI port's does for the functions behind its head. +#[test] +fn a_graphics_link_reset_resets_the_vpro() { + let board = fuel_with_graphics(crate::config::GraphicsBoard::V12); + let destination = VPRO_PIO | (ODSY_INT_DEST_LOWER + 4); + assert_eq!(board.io.pio_write(destination, 4, 0x55), BUS_OK); + assert_eq!(board.io.pio_read(destination, 4).data, 0x55); + let link_reset = 0x100 + 5 * 0x40 + 0x30 + 4; + assert_eq!(board.xbridge.crossbar().write32(link_reset, 1), BUS_OK); + assert_eq!(board.io.pio_read(destination, 4).data, 0); +} + +/// Without graphics nothing answers at widget 13 and its link is down, as +/// on a headless Fuel. +#[test] +fn a_fuel_without_graphics_has_no_widget_13() { + let board = fuel_with_graphics(crate::config::GraphicsBoard::None); + assert!(board.vpro.is_none()); + assert_ne!(board.io.pio_read(VPRO_PIO | 4, 4).status, BUS_OK); + assert_eq!(board.xbridge.crossbar().read32(VPRO_LINK_STATUS).data & (1 << 31), 0); +} + +/// IRIX's VPro driver writes each processor interface's graphics credit +/// counter at attach and tears the board down if it reads back zero; the +/// registers must keep what the guest writes, per interface. +#[test] +fn processor_interface_graphics_registers_keep_their_writes() { + let board = fuel_with_graphics(crate::config::GraphicsBoard::V12); + // Slot A's credit counter and bias; PI1 keeps its own block. + assert_eq!(board.pi[0].write64(0x308, 100), BUS_OK); + assert_eq!(board.pi[0].write64(0x310, 0x1000 | 68), BUS_OK); + assert_eq!(board.pi[0].read64(0x308).data, 100); + assert_eq!(board.pi[0].read64(0x310).data, 68); + assert_eq!(board.pi[1].read64(0x308).data, 0); + board.pi[0].power_on(); + assert_eq!(board.pi[0].read64(0x308).data, 0); +} diff --git a/src/ip35_machine.rs b/src/ip35_machine.rs index 87434a4b9..baac99fe6 100644 --- a/src/ip35_machine.rs +++ b/src/ip35_machine.rs @@ -4,8 +4,11 @@ //! Selected with `[machine] profile = "fuel_ip34"`. Shared keys select the //! PROM, NVRAM, memory and SCSI devices; `[fuel]` selects the serial console //! and writable flash. `ip35::Ip35Bus` maps the processor views over the -//! same Physical memory banks. Graphics is not attached, and the IOC3 has -//! no keyboard or mouse ports. +//! same Physical memory banks. The IOC3 carries PS/2 keyboard and mouse ports +//! (default) and the OHCI controller can take a USB pair; without graphics the +//! machine runs on a serial console. With `[graphics] board = "v12"` (or +//! `"v10"`) a VPro board answers at crossbar widget 13, the Fuel's graphics +//! slot. //! //! During PROM cache-as-memory bootstrap, cached startup bytes live in a //! Fuel cache adapter until a successful cached fetch leaves the PROM homes. @@ -18,10 +21,16 @@ //! at relative offsets 0 and 0x40 before DRAM discovery. Their hardware //! meaning is unknown; Bedrock logging records accesses to these windows. //! +//! The processor interfaces' graphics flow-control registers store what +//! IRIX writes, which is what its VPro driver checks, but count no stores: +//! no uncached store into the graphics page takes a credit and the board +//! returns none, so the credit-starvation interrupt never fires. The board's +//! DMA reaches memory through Bedrock's inbound path as widget 13. +//! //! Board composition ported from hw/mips/sgi/sn1-node.c //! in jamesbraid/qemu-sgi-origami. -use crate::config::{scsi_label, FuelConsole, MachineConfig}; +use crate::config::{scsi_label, FuelConsole, GraphicsBoard, MachineConfig}; use crate::cpu::mips_exec::{CpuDevice, MipsCpu, MipsCpuConfig, MipsExecutor}; use crate::cpu::mips_tlb::MipsTlb; use crate::dev::bedrock_flash::{Flash, PART_SIZE}; @@ -33,6 +42,7 @@ use crate::dev::bedrock_pi::{reg as pi_reg, BedrockPi, BedrockPiConfig, SIZE as use crate::dev::bedrock_xb::{BedrockXb, SIZE as XB_SIZE}; use crate::dev::heart::{host_clock, Clock}; use crate::dev::ioc3::{Ioc3, Ioc3Config}; +use crate::dev::odyssey::{Odyssey, PacketSender, PiGfx, PI_GFX_REGS}; use crate::dev::ioc3_eth::Ioc3Eth; use crate::dev::isp12160::Isp12160; use crate::dev::l1::{L1, L1Config, L1Event}; @@ -42,7 +52,7 @@ use crate::dev::prom_image; use crate::dev::ps2::Ps2Controller; use crate::dev::serial_host::{null_serial_backend, open_log, tcp_serial_backend, CiSerialBackend, LogMode, SerialBackend, TeeBackend}; use crate::dev::timekeeper::Timekeeper; -use crate::dev::xbridge::XBridge; +use crate::dev::xbridge::{InterruptPacket, XBridge}; use crate::devlog::{init_devlog, LogModule}; use crate::gdb_stub::CpuDebug; use crate::gfx_display::GfxDisplay; @@ -108,10 +118,15 @@ impl FuelMachine { revision: cfg.fuel.l1_revision.as_deref().map(crate::dev::l1::parse_revision).transpose()?, brick_serial: cfg.fuel.brick_serial.clone(), }, + graphics: cfg.graphics_board(), + graphics_pool: (cfg.graphics.threads, cfg.graphics.tile), + graphics_raster: (cfg.graphics.raster, cfg.graphics.gpu_backend.as_str()), })?); let timers = Arc::new(TimerManager::new()); let part = cfg.cpu(); let mut exec = board.build_cpu(part); + // The CPU's Compare matches blink the graphics status bar's LED. + exec.core.fasttick_count = board.fasttick.clone(); exec.core.set_timer_manager(timers.clone()); exec.core.set_count_hz( cfg.clock @@ -123,6 +138,8 @@ impl FuelMachine { let poll_board = board.clone(); exec.batch_hook = Some(Box::new(move |_| poll_board.poll())); let cpu: Arc = Arc::new(MipsCpu::<_, R12000ShadowCache>::new(*exec)); + board.status.set_cpu_cycles(cpu.cycles_ptr()); + board.status.set_count_hz(cpu.count_hz_atomic()); if let Some(space) = board.md.physical().ppmem_space() { use crate::ppmem::MappedMemory; unsafe { @@ -204,11 +221,9 @@ impl FuelMachine { monitor.register_device(PerfMonitor::new( cpu.running_flag(), cpu.cycles_ptr(), cpu.fasttick_count(), None, None, )); - monitor.register_device(board.md.physical().clone()); - monitor.register_device(board.l1.clone()); - monitor.register_device(board.scsi.clone()); - // `ps2 type` and friends: the keyboard and mouse from the monitor. - monitor.register_device(board.ioc3.ps2()); + for device in board.monitor_devices() { + monitor.register_device(device); + } cpu.register_locks(); board.scsi.register_locks(); board.ioc3.register_locks(); @@ -223,8 +238,10 @@ impl FuelMachine { board.eth.attach(link); } eprintln!( - "iris: Fuel IP34, {}, 512 MiB, serial console {:?}", - cfg.cpu().label(), cfg.fuel.console + "iris: Fuel IP34, {}, 512 MiB, {}, serial console {:?}", + cfg.cpu().label(), + if board.vpro.is_some() { cfg.graphics_board().label() } else { "no graphics" }, + cfg.fuel.console ); Ok(Self { board, @@ -369,6 +386,9 @@ impl Workstation for FuelMachine { self.board.eth.power_on(); self.board.scsi.power_on(); self.board.usb.power_on(); + if let Some(vpro) = &self.board.vpro { + vpro.power_on(); + } self.timekeeper.power_on(); self.restart_peripherals(); } @@ -405,7 +425,7 @@ impl Workstation for FuelMachine { self.lifecycle = state; } fn get_display(&self) -> Option> { - None + self.board.vpro.as_ref().map(|vpro| vpro.display()) } fn register_monitor_device(&self, device: Arc) { self.monitor.register_device(device); @@ -537,6 +557,15 @@ pub(crate) struct FuelBoard { pub eth: Arc, pub scsi: Arc, pub usb: Arc, + /// The VPro board in the graphics slot, widget 13. + pub vpro: Option>, + /// The status bar's activity bits and the CPU's Compare matches, which + /// the graphics board shows. + pub heartbeat: Arc, + pub fasttick: Arc, + /// What the graphics board's status bar shows: the activity bits, the + /// Compare matches and the CPU's cycle count and Count rate. + pub status: Arc, pub serial: [Arc; 3], #[cfg(test)] pub irq: Arc, @@ -557,6 +586,12 @@ pub(crate) struct FuelBoardConfig<'a> { pub usb_input: bool, /// Replacements for the L1's reported identity; unset keeps its defaults. pub l1: L1Identity, + /// `None`, `V12` or `V10`; validation refuses the others. + pub graphics: GraphicsBoard, + /// The VPro's fragment pipeline: worker count and tile size (`[graphics]`). + pub graphics_pool: (usize, u32), + /// Which unit draws and, for the GPU, the wgpu backend (`[graphics]`). + pub graphics_raster: (crate::config::RasterUnit, &'a str), } /// What a Fuel config may replace in the L1's identity replies. @@ -581,11 +616,14 @@ impl FuelBoard { ps2_input: true, usb_input: false, l1: L1Identity::default(), + graphics: GraphicsBoard::None, + graphics_pool: (2, 32), + graphics_raster: (crate::config::RasterUnit::Cpu, "auto"), }) } pub fn with_config(config: FuelBoardConfig<'_>) -> Result { - let FuelBoardConfig { payload, clock, flash_path, station_mac, timekeeper, ps2_input, usb_input, l1: l1_identity } = config; + let FuelBoardConfig { payload, clock, flash_path, station_mac, timekeeper, ps2_input, usb_input, l1: l1_identity, graphics, graphics_pool, graphics_raster } = config; let md = BedrockMd::with_clock(clock.clone()); let phys = md.physical().clone(); let pi: [Arc; 2] = std::array::from_fn(|interface| { @@ -778,6 +816,39 @@ impl FuelBoard { Isp12160::new_bus(), Isp12160::new_bus(), )); + let heartbeat = Arc::new(AtomicU64::new(0)); + let fasttick = Arc::new(AtomicU64::new(0)); + scsi.set_heartbeat(heartbeat.clone()); + ioc3.set_heartbeat(heartbeat.clone()); + eth.set_heartbeat(heartbeat.clone()); + let status = crate::disp::StatusSource::new(heartbeat.clone(), fasttick.clone()); + let vpro = if graphics == GraphicsBoard::None { + None + } else { + // Interrupt packets leave through the crossbar as the heads' do; + // DMA enters Bedrock as this widget's inbound traffic. + let crossbar = Arc::downgrade(&xbridge); + let packets: PacketSender = Arc::new(move |widget, address, vector, level| { + crossbar.upgrade().is_some_and(|x| x.send_interrupt(InterruptPacket { + source_widget: WIDGET_VPRO, + widget, + address, + vector, + level, + })) + }); + let dma = Arc::new(WidgetDma { io: Arc::downgrade(&io), widget: WIDGET_VPRO }); + let board = Odyssey::new_widget_relative( + WIDGET_VPRO, graphics.as_str(), packets, dma, status.clone(), graphics_pool, graphics_raster, + )?; + xbridge.attach_widget(WIDGET_VPRO, board.clone()); + io.connect_widget(WIDGET_VPRO, xbridge.widget(WIDGET_VPRO).expect("attached widget")); + Some(board) + }; + // Each processor interface keeps its own graphics registers. + for pi in &pi { + pi.set_node_hooks(Arc::new(PiGraphics::default())); + } xbridge.attach(15, 1, scsi.clone()); xbridge.attach(15, 4, ioc3.clone()); let usb = Ohci::new(); @@ -802,6 +873,7 @@ impl FuelBoard { let reset_eth = eth.clone(); let reset_scsi = scsi.clone(); let reset_usb = usb.clone(); + let reset_vpro = vpro.as_ref().map(Arc::downgrade); xbridge.set_reset_output(Arc::new(move |port| { if port == 15 { reset_ioc3.power_on(); @@ -809,6 +881,12 @@ impl FuelBoard { reset_scsi.power_on(); reset_usb.power_on(); } + // A link reset of the graphics port resets the board behind it. + if port == WIDGET_VPRO { + if let Some(vpro) = reset_vpro.as_ref().and_then(|v| v.upgrade()) { + vpro.power_on(); + } + } })); let mut bus = Ip35Bus::new(phys.clone()); #[cfg(feature = "jitv2")] @@ -886,6 +964,10 @@ impl FuelBoard { eth, scsi, usb, + vpro, + heartbeat, + fasttick, + status, serial, #[cfg(test)] irq, @@ -939,7 +1021,26 @@ impl FuelBoard { self.eth.start(); self.scsi.start(); self.usb.start(); + if let Some(vpro) = &self.vpro { + vpro.start(); + } + } + /// The board's devices that take monitor commands: the physical map, the + /// L1, the SCSI controller, the keyboard and mouse (`ps2 type` and + /// friends) and, when one is fitted, the VPro (`vpro …`). + pub fn monitor_devices(&self) -> Vec> { + let mut devices: Vec> = vec![ + self.md.physical().clone(), + self.l1.clone(), + self.scsi.clone(), + self.ioc3.ps2(), + ]; + if let Some(vpro) = &self.vpro { + devices.push(vpro.clone()); + } + devices } + pub fn poll(&self) { if self.running.load(Ordering::Acquire) { let now = (self.clock)(); @@ -958,6 +1059,9 @@ impl FuelBoard { self.running .store(false, Ordering::Release); self.md.stop_clock(); + if let Some(vpro) = &self.vpro { + vpro.stop(); + } self.usb.stop(); self.scsi.stop(); self.eth.stop(); @@ -969,6 +1073,55 @@ impl FuelBoard { } } +/// The crossbar widget of the Fuel's graphics slot: a Fuel IRIX inventory +/// lists its VPro under xtalk/13, and OpenBSD finds Odyssey at widget 13. +pub const WIDGET_VPRO: u8 = 13; + +/// One processor interface's graphics flow-control registers (0x300 to +/// 0x348): stored as written, with no store counting (see the module notes). +#[derive(Default)] +struct PiGraphics(Mutex); +impl crate::dev::bedrock_pi::NodeHooks for PiGraphics { + fn gfx_read(&self, index: usize) -> Option { + (index < PI_GFX_REGS as usize).then(|| self.0.lock().read(index as u32)) + } + fn gfx_write(&self, index: usize, value: u64) -> bool { + if index >= PI_GFX_REGS as usize { + return false; + } + self.0.lock().write(index as u32, value); + true + } + fn gfx_reset(&self) { + *self.0.lock() = PiGfx::default(); + } +} + +/// A widget's DMA as Bedrock's inbound path takes it: crosstalk addresses, +/// with the widget's inbound permissions applied. +struct WidgetDma { + io: std::sync::Weak, + widget: u8, +} +macro_rules! widget_dma_access { + ($r:ident,$w:ident,$ty:ty,$result:ident,$size:expr) => { + fn $r(&self, a: u64) -> $result { + let Some(io) = self.io.upgrade() else { return $result::err() }; + let r = io.inbound_read(self.widget, None, a, $size); + $result { data: r.data as $ty, status: r.status } + } + fn $w(&self, a: u64, v: $ty) -> u32 { + self.io.upgrade().map_or(BUS_ERR, |io| io.inbound_write(self.widget, None, a, $size, v as u64)) + } + }; +} +impl BusDevice for WidgetDma { + widget_dma_access!(read8, write8, u8, BusRead8, 1); + widget_dma_access!(read16, write16, u16, BusRead16, 2); + widget_dma_access!(read32, write32, u32, BusRead32, 4); + widget_dma_access!(read64, write64, u64, BusRead64, 8); +} + // XIO register packets reach only the two PI interrupt-modify registers. // The processor's register projection has a separate, wider interface. struct IoHub([Arc; 2]); From cd95ccc5b487b0f6f8c0106fc348f853ed0b6dc3 Mon Sep 17 00:00:00 2001 From: James Braid Date: Fri, 30 Oct 2026 08:13:37 +0000 Subject: [PATCH 20/20] docs: describe the Fuel machine Document the supported serial configuration, image inputs, the host input options and the explicit limits for graphics, USB devices and snapshots, in the feature list, hacking notes, debug notes, the sample config and a Fuel page. Startup storage remains a PROM-specific compatibility approximation. --- CHANGELOG.md | 8 ++++ FEATURES.md | 33 +++++++++++---- HACKING.md | 59 +++++++++++++++++--------- debug.md | 9 +++- docs/fuel-ip34.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++ fuel.toml.example | 44 ++++++++++++++++++++ 6 files changed, 228 insertions(+), 28 deletions(-) create mode 100644 docs/fuel-ip34.md create mode 100644 fuel.toml.example diff --git a/CHANGELOG.md b/CHANGELOG.md index ad05133d1..e9b05b4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -329,6 +329,14 @@ hashes are given where a change is easiest to understand by reading the commit. - A store-conditional on memory outside the window is now atomic with two CPUs. ### SGI Fuel (IP34 board, IP35 software platform) +- **The Fuel takes a VPro board**: `[graphics] board = "v12"` or `"v10"` + fits the Octane's VPro model at crossbar widget 13. IRIX 6.5.30 with the + VPro driver brings the X login screen up on it. + +- Added a headless Fuel profile with the 500 MHz R14000, 512 MiB RAM, + Bedrock, dual-head XBridge, IOC3 networking and two-channel ISP12160 SCSI. + The normal interpreter boot reaches the IRIX 6.5.30 root shell with PROM + 6.210. See the [Fuel guide](docs/fuel-ip34.md). - **jitv2 compiles the reset PROM from the PROM.** The PROM runs from the reset window uncached; once memory covers that window the cached view returns RAM there, and the compile pool used to compile RAM's bytes in place of the diff --git a/FEATURES.md b/FEATURES.md index b254836a7..316eddba6 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -175,15 +175,34 @@ the first `[section]`, because TOML tables continue until the next header. | Key | Default | Values and behavior | |---|---|---| -| `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)). | -| `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000. The first listed is the default. | +| `machine.profile` | `indy_ip24` | indy_ip24, indigo2_ip22, indigo2_ip28, o2_ip32 (configured by `[o2]`, see [the O2 guide](docs/o2-ip32.md)), octane_ip30 (configured by `[octane]`, see [the Octane guide](docs/octane-ip30.md)), fuel_ip34 (see [the Fuel guide](docs/fuel-ip34.md)). | +| `machine.cpu` | the machine's first | A family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`); independent of build features. Indy and `indigo2_ip22`: r4400, r5000; `indigo2_ip28`: r10000; O2: r5000, r10000, r12000; Octane: r10000, r12000, r14000; Fuel: r14000 (`r14000_500`). The first listed is the default. | | `machine.cpus` | `1` | Processors fitted. 2 only on the Octane; see [its guide](docs/Octane/multiprocessor.md). | +### [fuel] + +Fuel-specific settings apply when `machine.profile = "fuel_ip34"`. + +| Key | Default | Values and behavior | +|---|---|---| +| `flash` | unset | Persistent 16 MiB boot-flash file. Unset, the flash is volatile. | +| `console` | `l1` | `l1`, `ioc3a` or `ioc3b`. Selects the guest console connection. | +| `l1_port` | `8882` | Host TCP port for the L1 console. Used when `console = "l1"`. | +| `l1_log` | unset | Optional serial log path for L1 output when an IOC3 port is selected as console. | +| `l1_revision` | `1.48.1` | L1 firmware revision as `major.minor.patch`, decimal. The guest reads it with L1 request `0x0005`. | +| `brick_serial` | `EMUN00` | L1 brick serial, at most 37 bytes. The guest reads it with L1 request `0x0004`. The default is a marked synthetic serial, not a real one. | + +The machine uses one 500 MHz R14000 and fixed 512 MiB RAM in two 256 MiB +banks. Its IOC3 has no keyboard or mouse ports, so it runs on a serial +console. A `[fuel]` table is written to a saved config only when one of +these keys is set. The sample configuration and supported PROM inputs are in the +[Fuel guide](docs/fuel-ip34.md). + ### [graphics] | Key | Default | Values and behavior | |---|---|---| -| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). | +| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). The Fuel takes `v12` or `v10` in its graphics slot, or `none`, its default. | | `graphics.heads` | `1` | 1 or 2; dual-head requires Newport. | | `graphics.resolution` | `guest` | guest, 1024x768, 1280x960, 1280x1024; presets require Newport. | | `graphics.threads` | `2` | Worker threads the board's fragment pipeline draws on, at least 1. VPro: the screen tiles' owners, besides its front-end and raster threads; more help on a host with cores to spare. | @@ -218,7 +237,7 @@ An explicit non-disk kind takes precedence over the legacy cdrom flag. | Key | Default | Values and behavior | |---|---|---| -| `network.mode` | `nat` | nat, pcap, stream or off (no Ethernet; O2 and Octane only). Stream needs `network.stream`. PCAP needs build support and host capture permissions. PCAP works on every Ethernet; stream on every onboard one, not on a DaynaPort. | +| `network.mode` | `nat` | nat, pcap, stream or off (no Ethernet; O2, Octane and Fuel only). Stream needs `network.stream`. PCAP needs build support and host capture permissions. PCAP works on every Ethernet; stream on every onboard one, not on a DaynaPort. | | `network.stream` | `unset` | `tcp:IP:PORT` or `unix:PATH`: the server of the private segment for `mode = "stream"` (QEMU `-netdev stream` framing). See NETWORKING.md. | | `network.pcap_interface` | `auto-pick when unset` | Interface name or numeric list index; PCAP only. | | `network.nfs_pcap_ip` | `unset` | Virtual LAN IP for the built-in NFS responder in PCAP mode. | @@ -382,8 +401,8 @@ IP22/IP24 four-bank IRIX 6.5 behavior remains separate from IP28's verified two-bank 1 GB layout. Extreme requires IP22. All non-Newport boards require `heads=1` and `resolution="guest"`. IP28 IRIX needs IMPACT; its PROM has an embedded fallback. -The config validator permits SCSI controller 1 only on IP22, the O2 and the -Octane, the machines with two controllers. +The config validator permits SCSI controller 1 only on IP22, the O2, Octane +and Fuel. These machines have two controllers. Standalone CLI options update the loaded config where supplied; boolean enable switches do not provide a general way to clear every saved true setting. @@ -436,7 +455,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: a family (`r4400`, `r5000`, `r10000`, `r12000`, `r14000`) or a part (`r14000_600`) the machine takes. Overrides `[machine] cpu`. | -| `--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). | +| `--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, and `v10`/`v12` on the Fuel). | | `--raster` | VPro: `cpu` (default) or `wgpu`, the raster unit on the host GPU (`[graphics] raster`; needs a build with the `wgpu` feature). | | `--renderer` | What draws the window: `gl` (default) or `wgpu` (`renderer`; needs a build with the `wgpu` feature). | | `--gdb-port` | Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). Connect with: target remote localhost: | diff --git a/HACKING.md b/HACKING.md index 57830f44b..fed695211 100644 --- a/HACKING.md +++ b/HACKING.md @@ -16,13 +16,18 @@ Other documents: ## 1. What IRIS is -IRIS emulates five SGI workstations: the Indy (IP24), the Indigo2 (IP22), the -Indigo2 IMPACT (IP28), the O2 (IP32) and the Octane (IP30). The CPU is an -R4400, R5000, R10000, R12000 or R14000, chosen at runtime from the parts the -machine shipped with (`src/cpu/parts.rs`). -Graphics is Newport (REX3), GR2 (XZ/Extreme) or IMPACT on the Indy and -Indigo2, GBE on the O2 and Solid IMPACT on the Octane. It boots the real PROMs -and IRIX 5.3 and 6.5 to a desktop with networking. +IRIS emulates the Indy (IP24), Indigo2 (IP22), Indigo2 IMPACT (IP28), O2 +(IP32), Octane (IP30) and Fuel (IP34 board, IP35 software platform). The CPU +is an R4400, R5000, R10000, R12000 or R14000, chosen at runtime from the +parts the machine shipped with (`src/cpu/parts.rs`); the Fuel has its +500 MHz R14000. Graphics is Newport +(REX3), GR2 (XZ/Extreme) or IMPACT on the Indy and Indigo2, GBE on the O2, and +Solid IMPACT on the Octane. Fuel has no graphics board and runs headless on +its serial console. It does have a PS/2 keyboard and mouse on its IOC3, fed +from the host's input and the default as in the Fuel's manual, and `[fuel] +usb_input = true` adds a USB keyboard and mouse on its OHCI controller; +`ps2_input = false` turns PS/2 off. The graphical machines boot their real +PROMs and IRIX 5.3 or 6.5 to a desktop with networking. It is **not cycle-accurate**, anywhere. IRIX is interrupt-driven and does not care how many cycles an instruction takes, so IRIS models behaviour @@ -83,17 +88,18 @@ image. A CD-ROM's node is a `ChangerNode`: the queue of discs and the open active one, with the `Removable` trait (present, changed, eject, load) separate from `BlockNode` so that removable media which is not a block device, a tape, can use it too. The WD33C93A (Indy, Indigo2), the Adaptec -7880 (O2) and the ISP1040 (Octane) each make an empty bus of their own kind -(`new_bus`); the machine fills it from config and gives it to the controller's -constructor, so a controller exists with its targets and keeps only its -registers and DMA. What a `BlockNode` must guarantee about durable writes, -and why a disk's `write_cache` setting matters, is in the "Disk write cache" -section of [STORAGE.md](STORAGE.md) and the `block_node.rs` module docs: read -them before writing a backend or changing a flush. `src/scsi_monitor.rs` implements the monitor's `scsi` and `cow` +7880 (O2) and ISP1040 (Octane) each make an empty bus of their own kind +(`new_bus`). Fuel uses an ISP12160 with two SCSI buses behind one PCI +function. The machine fills each bus from config and gives it to the controller +constructor, so the controller keeps its registers and DMA. What a `BlockNode` +must guarantee about durable writes, and why a disk's `write_cache` setting +matters, is in the "Disk write cache" section of [STORAGE.md](STORAGE.md) and +the `block_node.rs` module docs: read them before writing a backend or changing +a flush. `src/scsi_monitor.rs` implements the monitor's `scsi` and `cow` subcommands and the run-time disc changes once, over the bus a controller hands -out through `ScsiTargets::scsi_bus`; chip-specific subcommands (`regs`; the -WD's `wdt`, `defer`, `debug`) stay with the chip, and `DiscChanger` is -implemented once for every `ScsiTargets`. Configuration is shared the same +out through `ScsiTargets::scsi_bus`. Chip-specific subcommands stay with the +chip, including `regs` and the WD's `wdt`, `defer` and `debug`. `DiscChanger` +is implemented once for every `ScsiTargets`. Configuration is shared the same 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. @@ -101,8 +107,9 @@ 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. +The ISP12160 defers target resets and releases its controller state lock +before walking the bus. It calls targets while holding the bus lock, so a +target callback must not take that lock again. Flash command engines share `FlashBacking` file storage while retaining their own command state. @@ -310,9 +317,12 @@ Every device that has real work to do runs on its own thread: | `VNC-Grab`, `VNC-Pointer`, `VNC-Client`/`VNC-Read` | with `--vnc`: frame diffing, pointer steering, one pair per viewer | | `machine-events` | power-off and reset requests from devices | | `ISP1040-0`, `ISP1040-1` | Octane SCSI request rings and disk transfers | -| `IOC3-RX-A`, `IOC3-RX-B`, `IOC3-NVRAM` | Octane serial input per port; the DS1685 NVRAM file writer (IOC3's other deadlines are `hptimer` one-shots) | +| `ISP12160-0` | Fuel SCSI request ring for both ISP12160 channels | +| `IOC3-RX-A`, `IOC3-RX-B`, `IOC3-NVRAM` | Octane and Fuel IOC3 serial input and NVRAM writer (IOC3's other deadlines are `hptimer` one-shots) | +| `l1-serial` | Fuel L1 console UART worker. Its other serial ports are IOC3 A and B | | `ip30-eth-rx`, `ip30-eth-net` | Octane ethernet receive pump; the network backend | | `odyssey-work`, `odyssey-raster`, `odyssey-pool-N`, `odyssey-display` | VPro: the front end decodes the queued command-FIFO stores (the processor's stores only queue); the raster unit sets each draw up and walks its primitives into 2x2 quads; a pool of workers (`[graphics] threads`, default 2) walks, shades and stores filled triangles and stores every other fragment, one screen tile at a time; and the display composes each whole frame from board memory (64-bit words, as main memory is: a field is a bit range of its word, read by shift and mask with no byte order, `vram.rs`) as the scan-out registers describe it, counts the retrace and presents the window at 60 Hz. No board-wide lock: the front end owns its state, an access that needs the drawing is answered busy until it is done, the display FIFO (PB&J's register file) has its own lock and never waits for the drawing (`display_fifo.rs`), and the display reads board memory while it is drawn, as the board's own scan-out does; nobody tracks which rows changed. The raster thread sets primitives up and writes each as one command in a ring of 64-byte slots (`pipe/ring.rs`): a triangle's record and bounding box, a quad it shaded itself (lines, points), or a clear's rectangle; the slot's index goes on the queue of every screen tile (`[graphics] tile`, default 32 pixels) it touches. A worker takes a ready tile and owns it until its queue is empty, walking each triangle within the tile and shading and storing quad by quad, so every pixel sees its fragments in command order with no other ordering, and the depth pre-test reads depth its own tile stored. A slot is retired once all its tiles have run it. The raster thread drains the ring before any other work on board memory (a texture upload waits for the shader alone). A completion (a fence's token, a query's words) does not drain: it rides the ring and runs once the ring retires past the quads queued before it. A quad's four pixels are lanes: the walk's depth, the texture unit's positions and the colour weights run on the host's SIMD unit (`fearless_simd`, `pipe/lanes.rs`, the level detected at start-up), bit for bit the scalar arithmetic. When its queue runs dry the raster thread does not drain: it keeps one count of the raster unit's pending jobs for the quads still in flight, and a front-end wait or a refused processor read asks it to store them. `IRIS_ODSY_SHADER=inline` makes the raster thread shade every quad itself, and `IRIS_ODSY_BACKEND=inline` stores on the raster thread with no pool. The texture registers a draw reads live in an array of draw state slots (`pipe/slot.rs`): the front end writes the current slot, a draw latches it and holds it until its job is given back, and a write while a queued draw holds the current slot first copies it to a free one, so the stages read their draw's registers with no lock | +| `ioc3-eth-rx`, `ioc3-eth-net` | Fuel IOC3 Ethernet receive pump and network backend | Threads that have nothing to do park instead of spinning: the graphics consumers when their FIFO is empty, and the CPU thread in `idle-pause` builds @@ -686,6 +696,15 @@ introspection commands are in [debug.md](debug.md#jit-v2). Crossbow and the Bridge with IOC3 and two ISP1040 SCSI controllers, an R10000, and IMPACT graphics (see the [Octane guide](docs/octane-ip30.md) for the boards). +- **IP34 board, IP35 software platform (Fuel)**: one 500 MHz R14000, 512 MiB + in two 256 MiB banks, Bedrock, two XBridge heads, IOC3 and a two-channel + ISP12160. It runs headless on the L1 or IOC3 serial console. The IOC3 uses + the shared `net::Link` for NAT or PCAP. The writable Bedrock flash and + serial-flash devices share `FlashBacking` storage, with separate command + handling. A VPro board (the Octane's Odyssey model) can sit at crossbar + widget 13; its packets and DMA go through the XBridge and Bedrock's inbound + path, and the PI graphics registers are stored but count no credits. + Snapshots are unsupported. See the [Fuel guide](docs/fuel-ip34.md). The Indy and Indigo2 profiles each have an embedded PROM fallback; `src/platform.rs` and `src/machine.rs` wire their differences, and diff --git a/debug.md b/debug.md index 129695cee..e0fbf5ab3 100644 --- a/debug.md +++ b/debug.md @@ -368,6 +368,13 @@ widget's link, refused Crossbow accesses, and accesses to physical addresses nothing is mapped at, with the PC; a device's own refusal is not listed). The `phys` commands work on the Octane as well. +On the Fuel the one controller is a two-channel ISP12160: `scsi` (or +`scsi0`) and `cow` act on channel 0, `scsi1` and `cow1` on channel 1, and +ids run 0 to 15 on each (`[scsi.17]` is id 1 on channel 1). `scsi notes` +shows the whole chip's command total and anything odd the driver did, and +has no `scsi1` form; `regs`, `wdt`, `defer`, `debug` and `dayna` are not +there. + Queue position `[0]` is the mounted disc, `[1]` the next on eject. A disc change reaches IRIX as a Unit Attention ("medium changed") on its next TEST UNIT READY; IRIX's `mediad` may still need a manual remount @@ -574,7 +581,7 @@ a forced termination; a normal stop saves them automatically. |---|---| | `phys mem [n]` / `mm` | Dump physical memory | | `phys dis [n]` / `md` | Disassemble physical memory | -| `phys map` | The decode table: pages by kind (untouched, filled but not yet mapped, shared, private), memory in use, slots faulted in, and the table as runs of devices. Every machine on `Physical` has the `phys` commands (Indy, Indigo2, O2, Octane) | +| `phys map` | The decode table: pages by kind (untouched, filled but not yet mapped, shared, private), memory in use, slots faulted in, and the table as runs of devices. Every machine on `Physical` has the `phys` commands (Indy, Indigo2, O2, Octane, Fuel) | | `phys trace ` / `trace ` | Log physical bus accesses | | `phys error ` | Stop on bus errors | | `phys hole ` | Stop on accesses to unmapped addresses | diff --git a/docs/fuel-ip34.md b/docs/fuel-ip34.md new file mode 100644 index 000000000..257125d8d --- /dev/null +++ b/docs/fuel-ip34.md @@ -0,0 +1,103 @@ +# SGI Fuel (IP34 board, IP35 software platform) + +Fuel is the IP34 board running the IP35 software platform. The emulated +configuration has one 500 MHz R14000 and 512 MiB of RAM in two fixed 256 MiB +banks. It includes Bedrock, two XBridge heads, an IOC3, and an ISP12160 with +two SCSI channels. The machine runs headless on its L1 or IOC3 serial console. + +## Configuration + +Copy [`fuel.toml.example`](../fuel.toml.example) to `fuel.toml`, provide the +PROM and disk files named there, then run `iris --config fuel.toml` from the +repository root. The sample selects `machine.profile = "fuel_ip34"` and +`machine.cpu = "r14000"`. The machine always runs its one R14000 (`r14000_500`, +the 500 MHz part); `cpu` may be left out, and any other value is refused. The sample +sets `banks = [256, 256]`, `headless = true`, `no_audio = true`, serial +ports 18880 and 18881, and `graphics.board = "none"`. The `[fuel]` settings +select the flash backing file, console, and optional L1 port and log. The +console options are `l1`, `ioc3a` and `ioc3b`. The Fuel's IOC3 has no +keyboard or mouse ports, so the console is always serial. + +The L1 answers firmware revision requests with 1.48.1 and brick serial +requests with the synthetic `EMUN00`; both are model choices, not values read +from a Fuel. `[fuel] l1_revision = "major.minor.patch"` and +`[fuel] brick_serial = "..."` replace them. Its system serial request returns +the machine's Ethernet address (`[network] mac`) as twelve uppercase +hexadecimal digits, the text the PROM converts back into that address. +IRIX's `numastatd` reads the revision at startup; without one it logs +`SNDRV_GET_ELSC_VERSION: I/O error`. + +The PROM decoder accepts a raw payload or a supported version-six, +single-segment stored or gzip container. The sample attaches a raw IRIX disk +at SCSI ID 1 and writes guest changes to its overlay. `[network] mode` defaults +to `nat`. `off` disconnects the link. `pcap` uses the shared `net::Link` backend +with a PCAP-enabled build and host capture support. The example sets `off`. +Set it to `nat` for the built-in gateway. + +## Observed boot and limits + +With the normal interpreter configuration, PROM 6.210 reports +`ALL DIAGS PASSED` and IRIX 6.5.30 has reached the root shell. The guest +reports a 500 MHz R14000 with 512 MiB, detects `ef0`, and passes the observed +NAT networking checks. During initial boot, IRIX's file indexer can keep the +guest busy while the serial console shows little activity. + +The PROM uses the R10000-family primary data cache as temporary memory before +it configures DRAM: it tags cache lines for the 32 KiB window +`0x100000`-`0x107fff` and installs them without a fill. The memory controller +models this from the cache's tag stores. PROM 6.210 stores, in order, test +patterns, an invalidate sweep, a valid tag (state 0xc0 or 0xc1) for each of the +window's eight pages, then an invalidate sweep. A valid tag for a page of the +window starts a private zero-filled page that answers the window on every path +(the cached path, uncached accesses and DMA), and keeps that 64 MiB region out +of the direct window; the first invalid tag ends it, and the window is memory +again. Power-on clears the page. This is the same mechanism the O2 uses for its +POST stack. Two things about it are approximated, not traced: the end is the +first invalid tag of the final sweep, not the last, which is right only if the +PROM does not use its stack between them; and the page answers uncached and DMA +accesses too, where the hardware has nothing behind the window to answer them. +A different PROM revision could tag another window or in another order. + +The ordinary interpreter configuration is the one with recorded Fuel boot +evidence. `lightning` and experimental `jitv2` builds are available, but no +Fuel guest boot with either is claimed here. + +## Graphics + +`[graphics] board = "v12"` (or `"v10"`) fits a VPro board in the graphics +slot, crossbar widget 13: a Fuel IRIX inventory lists its board under +xtalk/13, and Fuels have been reported with both the V12 and the V10. It is +the Octane's VPro model. Its interrupt packets cross the XBridge to Bedrock +like the PCI heads', its DMA enters Bedrock's inbound path as widget 13, and +a link reset of port 13 resets it. The processor interfaces' graphics +flow-control registers keep what IRIX writes but count no stores, so the +credit-starvation interrupt never fires. + +With PROM 6.210 the graphics diagnostic finds the board and passes. An IRIX +6.5.30 disk whose kernel includes the VPro driver lists `Graphics board: V12` +in `hinv` and brings the X login screen up on it. A disk installed for a +machine without graphics has no VPro driver in its kernel and runs as before. + +## Input + +The IOC3 carries a PS/2 keyboard and mouse, fed from host input; they are the +default, as in the Fuel's manual. `[fuel] usb_input = true` also plugs a USB +keyboard and mouse into the OHCI controller, and `ps2_input = false` leaves the +PS/2 pair out. Machine snapshots are unsupported. + +## Source basis + +The literal `0x53474972756c6573` is named `IIO_PROTECT_OVRRD_KEY` in public +Linux revision `db2ddb87143519e20a95aa36c60b36107b736a58`, +`arch/mips/include/asm/sn/sn0/hubio.h`. This establishes the literal's public +provenance, not the mapping of each Fuel register offset that uses it. Those +offsets remain approximate. The board composition port follows the project's +BSD-licensed QEMU SGI model in `hw/mips/sgi/sn1-node.c`. That source is a BSD-licensed implementation +reference, not proof of hardware behavior. + +The Hub-special head windows are compatibility approximations. Reads return +zero and writes are acknowledged without side effects. PROM 6.210 writes the +64-bit word `0x2000` at relative offsets `0x0` and `0x40` before discovering +DRAM; their hardware meaning remains unknown. Bedrock logging records +accesses. This keeps the existing model behavior explicit rather than +claiming a register implementation. diff --git a/fuel.toml.example b/fuel.toml.example new file mode 100644 index 000000000..8ac609a09 --- /dev/null +++ b/fuel.toml.example @@ -0,0 +1,44 @@ +# SGI Fuel (IP34 board, IP35 software platform), serial-only setup. +# PROM 6.210 and IRIX 6.5.30 have been observed reaching the root shell. +# Copy to fuel.toml and run from the repository root: iris --config fuel.toml +# Create ignore/fuel/ and supply your decoded raw PROM payload and IRIX disk. +# PROM also accepts supported version-six single-segment stored/gzip containers. +# No PROM or disk images are included. Convert QCOW2 disks to raw first. + +prom = "ignore/fuel/ip35prom.payload.bin" +nvram = "ignore/fuel/nvram.bin" +banks = [256, 256] +headless = true +no_audio = true +monitor_port = 18888 +serial_port_a = 18880 +serial_port_b = 18881 + +[machine] +profile = "fuel_ip34" +# The Fuel's one processor, the R14000 at 500 MHz with a 2 MiB secondary +# cache (`r14000_500`); other values are refused. Unset takes the same. +cpu = "r14000" + +[graphics] +# "v12" or "v10" fits a VPro in the graphics slot; IRIX needs a kernel with +# the VPro driver. The console stays on serial either way. +board = "none" + +[fuel] +flash = "ignore/fuel/flash.bin" +console = "l1" +l1_port = 18882 + +[network] +# Disconnected cable. For NAT, use mode = "nat". +# Keep the installed disk's working ef0 address and default route. +# NAT can adopt its static gateway before routing begins. +# Initial defaults: guest 192.168.0.2/24, gateway 192.168.0.1. +mode = "off" + +[scsi.1] +path = "ignore/fuel/irix.raw" +cdrom = false +# Keep the base disk unchanged. Guest writes go to irix.raw.overlay. +overlay = true