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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ jitv2 = ["cranelift-codegen", "cranelift-frontend", "cranelift-jit", "cranelift-
# specifically; opt in with --features jitv2_opcodefusion once it's earned
# more confidence.
jitv2_opcodefusion = ["jitv2"]
# IP28: the Indigo2 IMPACT machine (IP28) and its R10000 CPU. Without it the
# IP28 profile and the R10000 model are refused at startup with a message
# naming this feature, and none of their code is built, so nothing behind it
# can reach the R4400/R5000 machines. It implies ppmem: IP28 guest RAM is
# always mapped through the host MMU. Add jitv2 for the JIT, as elsewhere.
ip28 = ["ppmem"]
# jitv2 whole-page compile (§13, rules/jitv2/jit-v2-design.md): whole-page
# compilation is now the standard and only JIT v2 design. Kept as a compatibility
# alias for `jitv2` so downstream scripts and builds specifying `--features j2wp`
Expand Down
13 changes: 9 additions & 4 deletions cpu-tests/harness/console.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,22 @@ typedef long long s64;
* expectations are inferred, and docs/r4600.md says from what. */
#define CPU_R4600 0x4
#define CPU_ALL (CPU_R4400 | CPU_R5000 | CPU_R4600)
/* The R10000 is deliberately NOT in CPU_ALL: every CPU_ALL expectation was
* written against the three parts above, so a test runs on it only when it
* names it. */
#define CPU_R10000 0x8

extern u32 cpu_kind; /* CPU_R4400, CPU_R5000 or CPU_R4600, set at startup */
extern u32 cpu_kind; /* one CPU_* bit, set at startup */
extern u32 cpu_prid;
extern u32 cpu_fir;
extern u32 cpu_config;
static inline int is_r5000(void) { return cpu_kind == CPU_R5000; }
static inline int is_r4400(void) { return cpu_kind == CPU_R4400; }
static inline int is_r4600(void) { return cpu_kind == CPU_R4600; }
/* MIPS IV is the R5000's alone: the R4400 and the R4600 are both MIPS III
* parts and must refuse every MIPS IV encoding. */
static inline int has_mips4(void) { return cpu_kind == CPU_R5000; }
static inline int is_r10000(void) { return cpu_kind == CPU_R10000; }
/* MIPS IV: the R5000 and the R10000. The R4400 and the R4600 are both MIPS
* III parts and must refuse every MIPS IV encoding. */
static inline int has_mips4(void) { return cpu_kind == CPU_R5000 || cpu_kind == CPU_R10000; }

/* ── Console ──────────────────────────────────────────────────────────────── */
void con_init(void);
Expand Down
1 change: 1 addition & 0 deletions cpu-tests/harness/iris.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
#define IMP_R4700 0x21
#define IMP_R4650 0x22
#define IMP_R5000 0x23
#define IMP_R10000 0x09
#define IMP_RM7000 0x27
#define IMP_RM5200 0x28

Expand Down
2 changes: 2 additions & 0 deletions cpu-tests/harness/testlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ static void identify(void)
* docs/r4600.md for where each R4600 answer comes from. */
case IMP_R4600: cpu_kind = CPU_R4600; break;
case IMP_R5000: cpu_kind = CPU_R5000; break;
case IMP_R10000: cpu_kind = CPU_R10000; break;
default: cpu_kind = 0; break;
}
}
Expand All @@ -188,6 +189,7 @@ static const char *cpu_name(void)
if (cpu_kind == CPU_R4400) return "R4400";
if (cpu_kind == CPU_R5000) return "R5000";
if (cpu_kind == CPU_R4600) return "R4600";
if (cpu_kind == CPU_R10000) return "R10000";
return "unknown";
}

Expand Down
17 changes: 17 additions & 0 deletions ip28.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Indigo2 IMPACT R10000 (IP28) bring-up.
# Runs the real IP28 PROM against the existing fullhouse (IP22) machine.
# The CPU is still an R4400/R5000 model — the point of this run is to find
# out what the PROM objects to first, not to boot anything.
headless = true
no_audio = true
banks = [64, 64, 64, 64]
prom = "ip28/ip28prom.070-1477-002.bin"
nveeprom = "ip28/nveeprom-ip28.bin"
serial_log = "ip28/console-ip28.log"
[machine]
profile = "indigo2_ip22"
cpu = "r10000"
[scsi.1]
path = "ip28/blank.raw"
cdrom = false
overlay = true
97 changes: 77 additions & 20 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;

/// Valid memory bank sizes in MB.
pub const VALID_BANK_SIZES: &[u32] = &[0, 8, 16, 32, 64, 128];
pub const VALID_BANK_SIZES: &[u32] = &[0, 8, 16, 32, 64, 128, 256];

/// What sits at a SCSI id. `cdrom = true` remains the historical spelling for
/// `kind = "cdrom"`; either works and they mean the same thing.
Expand Down Expand Up @@ -384,28 +384,53 @@ pub enum MachineProfile {
IndyIp24,
/// SGI Indigo2 IP22 — fullhouse MC/IOC, Newport XL on GIO gfx slot.
Indigo2Ip22,
/// SGI Indigo2 IMPACT IP28 — an R10000 CPU module in the Indigo2 chassis.
///
/// Shares the fullhouse MC/IOC/HPC3 with IP22 and differs in the decodes
/// inside them: MEMCFG's base field is shifted by 24 rather than 22 (so
/// its size granule is 16 MB, not 4), RAM lives at 0x20000000 with the
/// low-memory alias following it there, and both the MC chip revision and
/// the HPC3 board revision have to read high enough for the kernel to
/// call the board an IP28.
///
/// Graphics is IMPACT, which is a register stub — an IP28 kernel carries
/// no Newport driver, so REX3 is not an alternative here.
Indigo2Ip28,
}

impl MachineProfile {
/// All selectable profiles, in display order. Single source of truth for the
/// GUI dropdowns (Config tab + New Machine dialog) so they never drift.
#[cfg(feature = "ip28")]
pub const ALL: [Self; 3] = [Self::IndyIp24, Self::Indigo2Ip22, Self::Indigo2Ip28];
#[cfg(not(feature = "ip28"))]
pub const ALL: [Self; 2] = [Self::IndyIp24, Self::Indigo2Ip22];

pub fn label(self) -> &'static str {
match self {
Self::IndyIp24 => "SGI Indy (IP24)",
Self::Indigo2Ip22 => "SGI Indigo2 (IP22)",
Self::Indigo2Ip28 => "SGI Indigo2 IMPACT (IP28)",
}
}

pub fn supported(self) -> bool {
matches!(self, Self::IndyIp24 | Self::Indigo2Ip22)
|| (cfg!(feature = "ip28") && matches!(self, Self::Indigo2Ip28))
}

/// MC/IOC/HPC3 Guinness vs Fullhouse layout. Indy IP24 is Guinness (`true`).
pub fn guinness(self) -> bool {
matches!(self, Self::IndyIp24)
}

/// The R10000 Indigo2. Selects the IP28 decodes inside the shared
/// fullhouse devices — see the variant's own documentation for the list.
///
/// Always false without the `ip28` feature, so every IP28 decode folds away.
pub fn ip28(self) -> bool {
cfg!(feature = "ip28") && matches!(self, Self::Indigo2Ip28)
}
}

/// Indy / Indigo2 graphics board in the GIO gfx slot.
Expand Down Expand Up @@ -469,23 +494,10 @@ impl ImpactSection {
|| self.exp1 != ImpactSlot::None
}

/// Hardware-valid slot population (rejects High+High and orphan expansion boards).
/// One IMPACT board, in the graphics slot; a second head is not modelled yet.
pub fn validate(&self) -> Result<(), String> {
let slots = [self.gfx, self.exp0, self.exp1];
let high_count = slots.iter().filter(|&&s| s == ImpactSlot::High).count();
if high_count >= 2 {
return Err(
"[impact] High+High is invalid — at most one High IMPACT board per system".into(),
);
}
if self.exp0 != ImpactSlot::None && self.gfx == ImpactSlot::None {
return Err("[impact] exp0 requires gfx slot populated".into());
}
if self.exp1 != ImpactSlot::None && self.exp0 == ImpactSlot::None {
return Err("[impact] exp1 requires exp0 populated (Maximum IMPACT uses all three slots)".into());
}
if self.exp1 == ImpactSlot::Max && self.exp0 != ImpactSlot::High {
return Err("[impact] Maximum IMPACT expects exp0=high when exp1=max".into());
if self.exp0 != ImpactSlot::None || self.exp1 != ImpactSlot::None {
return Err("[impact] only the graphics slot (gfx) is supported so far".into());
}
Ok(())
}
Expand Down Expand Up @@ -538,12 +550,27 @@ pub enum CpuModel {
R4400,
/// MIPS R5000, 2-way 32K L1s, no secondary cache, MIPS IV.
R5000,
/// MIPS R10000, 2-way 32K L1s, 1 MB secondary cache, MIPS IV. The CPU in
/// the Indigo2 IMPACT (IP28). Bring-up only — see docs/ip28-bringup.md.
R10000,
}

impl CpuModel {
#[cfg(feature = "ip28")]
pub const ALL: [Self; 3] = [Self::R4400, Self::R5000, Self::R10000];
#[cfg(not(feature = "ip28"))]
pub const ALL: [Self; 2] = [Self::R4400, Self::R5000];

/// Whether this build can run the model: the R10000 needs the `ip28` feature.
pub fn available(self) -> bool {
!matches!(self, Self::R10000) || cfg!(feature = "ip28")
}
pub fn label(self) -> &'static str {
match self { Self::R4400 => "MIPS R4400", Self::R5000 => "MIPS R5000" }
match self {
Self::R4400 => "MIPS R4400",
Self::R5000 => "MIPS R5000",
Self::R10000 => "MIPS R10000",
}
}
}

Expand Down Expand Up @@ -1218,6 +1245,11 @@ impl MachineConfig {

/// Validate bank sizes, returns a description of any errors.
pub fn validate(&self) -> Result<(), String> {
if (self.machine.profile == MachineProfile::Indigo2Ip28 && !cfg!(feature = "ip28"))
|| !self.machine.cpu.available()
{
return Err("IP28 / R10000 support is not built into this binary; rebuild with --features ip28".to_string());
}
if !self.machine.profile.supported() {
return Err(format!(
"machine profile \"{}\" is not implemented; use {}",
Expand Down Expand Up @@ -1248,9 +1280,11 @@ impl MachineConfig {
return Err(format!("graphics.board \"{name}\" and [impact] both claim the GIO gfx slot"));
}
}
if self.impact.any_enabled() && self.machine.profile != MachineProfile::Indigo2Ip22 {
if self.impact.any_enabled()
&& !matches!(self.machine.profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28)
{
return Err(
"[impact] slots are preview-only on Indigo2 (machine.profile = indigo2_ip22)".into(),
"[impact] slots need an Indigo2 (machine.profile = indigo2_ip22 or indigo2_ip28)".into(),
);
}
self.impact.validate()?;
Expand All @@ -1275,6 +1309,14 @@ impl MachineConfig {
i, sz, VALID_BANK_SIZES
));
}
// The IP22/IP24 MC cannot express a 256 MB bank at its base
// shift; only the IP28's can.
if sz == 256 && !self.machine.profile.ip28() {
return Err(format!(
"bank{} size 256 MB needs the IP28 (machine.profile = indigo2_ip28)",
i
));
}
}
if let Some(ref s) = self.nat_subnet {
if let Err(e) = parse_nat_subnet(s) {
Expand Down Expand Up @@ -1823,6 +1865,21 @@ mod export_tests {
assert!(cfg.machine.profile.supported());
assert!(!cfg.machine.profile.guinness());
}

#[test]
fn a_256_mb_bank_is_an_ip28_bank() {
let mut cfg = MachineConfig::default();
cfg.machine.profile = MachineProfile::Indigo2Ip22;
cfg.banks = [256, 128, 0, 0];
let err = cfg.validate().expect_err("the IP22 MC cannot express a 256 MB bank");
assert!(err.contains("256 MB"), "{err}");
#[cfg(feature = "ip28")]
{
cfg.machine.profile = MachineProfile::Indigo2Ip28;
cfg.machine.cpu = CpuModel::R10000;
cfg.validate().expect("the IP28 MC can");
}
}
}

#[cfg(test)]
Expand Down
6 changes: 4 additions & 2 deletions src/devlog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ impl LogModule {
if mask & 0x0002 != 0 { parts.push("tlb"); }
if mask & 0x0004 != 0 { parts.push("mem"); }
if mask & 0x0008 != 0 { parts.push("fpu"); }
let rest = mask & !0x000F;
if mask & 0x0010 != 0 { parts.push("cp0"); }
let rest = mask & !0x001F;
let mut s = parts.join("+");
if rest != 0 { s.push_str(&format!("+{:#010x}", rest)); }
if s.is_empty() { format!("{:#010x}", mask) } else { s }
Expand Down Expand Up @@ -192,6 +193,7 @@ impl LogModule {
"tlb" => Some(0x0002),
"mem" => Some(0x0004),
"fpu" => Some(0x0008),
"cp0" => Some(0x0010),
"on" | "all" => Some(0xFFFF_FFFF),
"off" | "none" => Some(0x0000_0000),
_ => u32::from_str_radix(s.trim_start_matches("0x"), 16).ok(),
Expand Down Expand Up @@ -458,7 +460,7 @@ impl Device for DevLog {
"[DEV] log <module|all> <on|off> | log <module> mask <cat|hex> | log <module> file <path|off> | log status\n\
\x20 modules: net hpc3 seeq hal2 mc rex3 mips ioc scsi pdma vino dcb vc2 cmap xmap bt445 scc ps2 rtc eeprom l1i l1d l2c\n\
\x20 pdma mask categories: hal enet scsi on/all off/none <hex>\n\
\x20 mips mask categories: insn tlb mem fpu on/all off/none <hex>\n\
\x20 mips mask categories: insn tlb mem fpu cp0 on/all off/none <hex>\n\
\x20 l1i/l1d/l2c mask categories: hit miss op on/all off/none <hex>\n\
\x20 [DEV] = requires --features developer build to produce output".to_string(),
)]
Expand Down
43 changes: 42 additions & 1 deletion src/ioc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,10 +384,15 @@ struct IocIrqLine {
struct IocTimerCallback {
state: Arc<Mutex<IocState>>,
source: IocInterrupt,
/// The status bar's clock-tick counter (see `Ioc::set_clock_ticks`).
ticks: Arc<std::sync::OnceLock<Arc<AtomicU64>>>,
}

impl TimerCallback for IocTimerCallback {
fn callback(&self) {
if let Some(t) = self.ticks.get() {
t.fetch_add(1, Ordering::Relaxed);
}
let mut state = self.state.lock();
match self.source {
IocInterrupt::Mappable0 => state.map_stat |= 1 << 0,
Expand Down Expand Up @@ -427,6 +432,8 @@ pub struct Ioc {
event_tx: Arc<std::sync::OnceLock<mpsc::SyncSender<MachineEvent>>>,
/// Shared heartbeat — IOC sets/clears HB_LED_RED/GREEN bits directly.
heartbeat: Arc<std::sync::OnceLock<Arc<AtomicU64>>>,
/// Counter bumped on every 8254 timer 0/1 interrupt (see `set_clock_ticks`).
clock_ticks: Arc<std::sync::OnceLock<Arc<AtomicU64>>>,
/// Shared timer manager for PIT channels.
timer_manager: Arc<std::sync::OnceLock<Arc<TimerManager>>>,
}
Expand All @@ -444,8 +451,29 @@ impl Ioc {
Self::new_inner(guinness, true)
}

/// As `new`/`new_ci`, with the machine profile's IP28 flag: an IP28
/// baseboard must report a high enough HPC3 board revision.
pub fn new_for_profile(guinness: bool, ci_mode: bool, ip28: bool) -> Self {
Self::new_inner_profile(guinness, ci_mode, ip28)
}

fn new_inner(guinness: bool, ci_mode: bool) -> Self {
let sys_id = if guinness { 0x26 } else { 0x11 }; // primarily prom looks at bit 1 to detect full house.
Self::new_inner_profile(guinness, ci_mode, false)
}

fn new_inner_profile(guinness: bool, ci_mode: bool, ip28: bool) -> Self {
// HPC3 SYS_ID: [7:5] chip rev, [4:1] board rev, [0] 1 = fullhouse.
// The PROM looks at bit 0 to tell fullhouse from guinness. IRIX reads
// the board revision to tell an IP28 baseboard from an IP26 one and
// warns "CPU baseboard downrev (IP26 not IP28)" below 13, so an IP28
// has to report at least that.
let sys_id: u8 = if guinness {
0x26
} else if ip28 {
0x1B // board rev 13, fullhouse
} else {
0x11 // board rev 8, fullhouse
};
let state = Arc::new(Mutex::new(IocState {
sys_id,
l0_stat: 0,
Expand Down Expand Up @@ -475,14 +503,17 @@ impl Ioc {
source: IocInterrupt::Serial,
});

let clock_ticks = Arc::new(std::sync::OnceLock::new());
let timer0_cb = Arc::new(IocTimerCallback {
state: state.clone(),
source: IocInterrupt::Mappable0,
ticks: clock_ticks.clone(),
});

let timer1_cb = Arc::new(IocTimerCallback {
state: state.clone(),
source: IocInterrupt::Mappable1,
ticks: clock_ticks.clone(),
});

let ps2_cb = Arc::new(IocIrqLine {
Expand All @@ -504,6 +535,7 @@ impl Ioc {
guinness,
event_tx: Arc::new(std::sync::OnceLock::new()),
heartbeat: Arc::new(std::sync::OnceLock::new()),
clock_ticks,
timer_manager: Arc::new(std::sync::OnceLock::new()),
}
}
Expand All @@ -517,6 +549,15 @@ impl Ioc {
let _ = self.event_tx.set(tx);
}

/// Count the kernel's clock ticks into the status bar's Hz counter. IRIX
/// keeps time with the 8254 (timer 0 is the system clock, timer 1 the
/// profiling clock) unless the IOC's 8254 is known broken, in which case
/// it uses CP0 Compare, which the CPU already counts. Either way the
/// counter shows the kernel's tick rate, and never both at once.
pub fn set_clock_ticks(&self, ticks: Arc<AtomicU64>) {
let _ = self.clock_ticks.set(ticks);
}

pub fn set_heartbeat(&self, heartbeat: Arc<AtomicU64>) {
let _ = self.heartbeat.set(heartbeat);
}
Expand Down
Loading
Loading