diff --git a/src/dev/hpc3.rs b/src/dev/hpc3.rs index 7422eecc..0981bdf7 100644 --- a/src/dev/hpc3.rs +++ b/src/dev/hpc3.rs @@ -11,8 +11,9 @@ use crate::dev::eeprom_93c56::Eeprom93c56; use crate::dev::ioc::Ioc; use crate::dev::ds1x86::Ds1x86; use crate::net::GatewayConfig; -use crate::dev::seeq8003::{Seeq8003, SeeqCallback}; -use crate::dev::wd33c93a::{Wd33c93a, ScsiCallback}; +use crate::dev::seeq8003::Seeq8003; +use crate::dev::wd33c93a::Wd33c93a; +use crate::irq::{InterruptController, IrqLine}; use crate::dev::ioc::IocInterrupt; use crate::dev::hal2::Hal2; use crate::hptimer::TimerManager; @@ -239,77 +240,89 @@ struct Hpc3State { pbus_pio: [u32; 0x1000], } -pub trait PdmaCallback: Send + Sync { - fn set_dma_interrupt(&self, level: bool); -} - - -struct Hpc3Irq { +/// The HPC3's interrupt status (INTSTAT) as a controller: line `n` is +/// INTSTAT bit `n`. SCSI channel 0's chip and DMA bits are ORed onto the +/// IOC's SCSI0 line and channel 1's onto SCSI1; the SEEQ's goes to Ethernet. +struct Hpc3Interrupts { state: Arc>, - ioc: Ioc, - bit: u32, - ioc_line: IocInterrupt, - /// For SCSI chip-IRQ wirings: the paired PDMA channel + its DMA-side - /// intstat bit, so that a chip-INT ack also drops the PDMA half of the - /// shared SCSI INT3 line. None for callbacks that don't have a paired - /// PDMA channel (e.g. PDMA-side Hpc3Irq, ethernet, …). - pdma_paired: Option<(Arc>, u32)>, + /// The IOC's SCSI0, SCSI1 and Ethernet lines. + scsi: [IrqLine; 2], + ethernet: IrqLine, + /// The SCSI PDMA channels, so that a chip-INT ack can also drop the PDMA + /// half of the shared SCSI line (see `set_line`). + scsi_dma: [Arc>; 2], } -impl Hpc3Irq { - fn update(&self, level: bool) { +impl Hpc3Interrupts { + /// The line for one INTSTAT bit. + fn line(self: &Arc, bit: u32) -> IrqLine { + IrqLine::new(self.clone(), bit.trailing_zeros()) + } + + /// Set or clear `bit`, then drive SCSI channel `n`'s IOC line from both + /// of its bits. + fn update_scsi(&self, n: usize, bit: u32, level: bool) { let mut state = self.state.lock(); - if level { state.intstat |= self.bit; } else { state.intstat &= !self.bit; } - - // Determine IOC line state based on all contributors - let active = match self.ioc_line { - IocInterrupt::Scsi0 => (state.intstat & (HPC3_INTSTAT_SCSI0_DEV | HPC3_INTSTAT_SCSI0_DMA)) != 0, - IocInterrupt::Scsi1 => (state.intstat & (HPC3_INTSTAT_SCSI1_DEV | HPC3_INTSTAT_SCSI1_DMA)) != 0, - IocInterrupt::Ethernet => (state.intstat & (HPC3_INTSTAT_ENET_DEV | HPC3_INTSTAT_ENET_RX_DMA | HPC3_INTSTAT_ENET_TX_DMA)) != 0, - _ => false, - }; - self.ioc.set_interrupt(self.ioc_line, active); + if level { state.intstat |= bit; } else { state.intstat &= !bit; } + self.scsi[n].set(state.intstat & SCSI_BITS[n] != 0); } } - -impl ScsiCallback for Hpc3Irq { - fn set_interrupt(&self, level: bool) { - self.update(level); - if !level { - // When the chip-side INT deasserts, also drop any pending PDMA - // DMA-completion bit on the same SCSI line — on real HPC3 the SCSI - // INT3 source is shared and the chip ack settles both halves. - // Without this, intstat[SCSI*_DMA] stays asserted forever when the - // IRIX miniroot's SCSI driver only acks via the chip path. - if let Some((chan, dma_bit)) = &self.pdma_paired { - let mut c = chan.lock(); - if c.ctrl & PDMA_CTRL_INT != 0 { - c.ctrl &= !PDMA_CTRL_INT; +/// Each SCSI channel's chip and DMA bits, which share its IOC line. +const SCSI_BITS: [u32; 2] = [ + HPC3_INTSTAT_SCSI0_DEV | HPC3_INTSTAT_SCSI0_DMA, + HPC3_INTSTAT_SCSI1_DEV | HPC3_INTSTAT_SCSI1_DMA, +]; + +impl InterruptController for Hpc3Interrupts { + fn set_line(&self, line: u32, level: bool) { + let bit = 1u32 << line; + match bit { + HPC3_INTSTAT_SCSI0_DMA => self.update_scsi(0, bit, level), + HPC3_INTSTAT_SCSI1_DMA => self.update_scsi(1, bit, level), + HPC3_INTSTAT_SCSI0_DEV | HPC3_INTSTAT_SCSI1_DEV => { + let (n, dma_bit) = if bit == HPC3_INTSTAT_SCSI0_DEV { + (0, HPC3_INTSTAT_SCSI0_DMA) + } else { + (1, HPC3_INTSTAT_SCSI1_DMA) + }; + self.update_scsi(n, bit, level); + if !level { + // When the chip-side INT deasserts, also drop any pending + // PDMA DMA-completion bit on the same SCSI line: on a real + // HPC3 the SCSI INT3 source is shared and the chip ack + // settles both halves. Without this, intstat[SCSI*_DMA] + // stays asserted forever when the IRIX miniroot's SCSI + // driver only acks via the chip path. + { + let mut c = self.scsi_dma[n].lock(); + if c.ctrl & PDMA_CTRL_INT != 0 { + c.ctrl &= !PDMA_CTRL_INT; + } + } + let mut st = self.state.lock(); + st.intstat &= !dma_bit; + let active = st.intstat & SCSI_BITS[n] != 0; + drop(st); + self.scsi[n].set(active); + } + } + HPC3_INTSTAT_ENET_DEV => { + { + let mut st = self.state.lock(); + if level { st.intstat |= HPC3_INTSTAT_ENET_DEV; } + else { st.intstat &= !HPC3_INTSTAT_ENET_DEV; } } - drop(c); - let mut st = self.state.lock(); - st.intstat &= !*dma_bit; - let active = (st.intstat & (HPC3_INTSTAT_SCSI0_DEV | HPC3_INTSTAT_SCSI0_DMA - | HPC3_INTSTAT_SCSI1_DEV | HPC3_INTSTAT_SCSI1_DMA)) - & match self.ioc_line { - IocInterrupt::Scsi0 => HPC3_INTSTAT_SCSI0_DEV | HPC3_INTSTAT_SCSI0_DMA, - IocInterrupt::Scsi1 => HPC3_INTSTAT_SCSI1_DEV | HPC3_INTSTAT_SCSI1_DMA, - _ => 0, - }; - drop(st); - self.ioc.set_interrupt(self.ioc_line, active != 0); + dlog_dev!(LogModule::Hpc3, "ENET IRQ: level={}", level); + // The SEEQ's own level, as it always was: not ORed with the + // Ethernet DMA bits. + self.ethernet.set(level); } + _ => {} } } } -impl PdmaCallback for Hpc3Irq { - fn set_dma_interrupt(&self, level: bool) { - self.update(level); - } -} - struct PdmaChannel { id: usize, /// Device-side reads and writes since power-on (see `pdma status`). @@ -341,7 +354,8 @@ struct PdmaChannel { sys_mem: Option>, endian: bool, even_high: bool, // PBUS only: even bytes on high bus lane (bit 19 of dmacfg) - callback: Option>, + /// The channel's DMA-completion interrupt (SCSI channels only). + irq: Option, dump_enabled: Arc, dump_file: Option, dump_transaction_id: u32, @@ -377,7 +391,7 @@ impl PdmaChannel { sys_mem: None, endian: false, even_high: false, - callback: None, + irq: None, dump_enabled, dump_file: None, dump_transaction_id: 0, @@ -479,8 +493,8 @@ fn start_transaction(&mut self) { if self.log_active() { dlog_dev!(LogModule::Pdma, "PDMA[{}]: Transfer Complete (EOX), Interrupting", self.id); } self.ctrl |= PDMA_CTRL_INT; // Set interrupt pending self.trace_int(); - if let Some(cb) = &self.callback { - cb.set_dma_interrupt(true); + if let Some(irq) = &self.irq { + irq.set(true); } } break; // Done @@ -733,9 +747,9 @@ fn start_transaction(&mut self) { status |= DmaStatus(DmaStatus::IRQ); // For SCSI/PBUS channels (callback installed): set ctrl INT flag and notify. // For enet channels (no callback): IRQ bit in status is the signal; don't touch ctrl. - if let Some(cb) = &self.callback { + if let Some(irq) = &self.irq { self.ctrl |= PDMA_CTRL_INT; - cb.set_dma_interrupt(true); + irq.set(true); } if self.log_active() { dlog_dev!(LogModule::Pdma, "PDMA[{}]: Interrupting (xie caller_eop={} bc_done={})", self.id, caller_eop, bc_done); } } @@ -797,8 +811,8 @@ impl PdmaChannelOps for PbusDmaOps { let val = chan.ctrl; if (chan.ctrl & PDMA_CTRL_INT) != 0 { chan.ctrl &= !PDMA_CTRL_INT; - if let Some(cb) = &chan.callback { - cb.set_dma_interrupt(false); + if let Some(irq) = &chan.irq { + irq.set(false); } } val @@ -847,8 +861,8 @@ impl PdmaChannelOps for ScsiDmaOps { let val = chan.ctrl; if (chan.ctrl & SCSI_CTRL_INT) != 0 { chan.ctrl &= !SCSI_CTRL_INT; - if let Some(cb) = &chan.callback { - cb.set_dma_interrupt(false); + if let Some(irq) = &chan.irq { + irq.set(false); } } val @@ -925,26 +939,6 @@ const ENET_BC_TXD: u16 = 0x8000; // BC register bit 28: HPC3 has sampled EOX from the current descriptor const ENET_BC_EOX_SAMPLED: u32 = 0x10000000; -/// Ethernet SEEQ interrupt callback. -/// Raises/lowers the IOC Ethernet line and updates intstat. -/// INTPEND (bit 1 of ENET_RX_RESET register) is mirrored in enet_intpend (AtomicBool) -/// so it can be read lock-free by ENET_RX_RESET register reads (which hold the channel lock). -struct EnetSeeqIrq { - hpc3_state: Arc>, - ioc: Ioc, -} -impl SeeqCallback for EnetSeeqIrq { - fn set_interrupt(&self, level: bool) { - { - let mut st = self.hpc3_state.lock(); - if level { st.intstat |= HPC3_INTSTAT_ENET_DEV; } - else { st.intstat &= !HPC3_INTSTAT_ENET_DEV; } - } - dlog_dev!(LogModule::Hpc3, "ENET IRQ: level={}", level); - self.ioc.set_interrupt(IocInterrupt::Ethernet, level); - } -} - struct EnetRxDmaOps { seeq: Arc>>, } @@ -1164,19 +1158,6 @@ impl Hpc3 { chan.active_mask = ENET_TX_CTRL_ACTIVE; // 0x200 for enet RX/TX } - // Setup DMA interrupts - if i == HPC3_PDMA_CHAN_SCSI0 as usize { - chan.callback = Some(Arc::new(Hpc3Irq { - state: state.clone(), ioc: ioc.clone(), bit: HPC3_INTSTAT_SCSI0_DMA, ioc_line: IocInterrupt::Scsi0, - pdma_paired: None, // PDMA-side: doesn't itself need to clear another PDMA - })); - } else if i == HPC3_PDMA_CHAN_SCSI1 as usize { - chan.callback = Some(Arc::new(Hpc3Irq { - state: state.clone(), ioc: ioc.clone(), bit: HPC3_INTSTAT_SCSI1_DMA, ioc_line: IocInterrupt::Scsi1, - pdma_paired: None, - })); - } - // Enet channels 10/11: no DMA completion callback — interrupt is driven by SEEQ via EnetSeeqIrq pdma_channels.push(Arc::new(Mutex::new(chan))); dma_clients.push(Arc::new(PdmaClientImpl { channel: pdma_channels.last().unwrap().clone() })); if i <= HPC3_PDMA_CHAN_GENERIC as usize { @@ -1197,10 +1178,18 @@ impl Hpc3 { let enet_rx_dma = Arc::new(PdmaClientImpl { channel: pdma_channels[10].clone() }); let enet_tx_dma = Arc::new(PdmaClientImpl { channel: pdma_channels[11].clone() }); - let seeq_irq = Arc::new(EnetSeeqIrq { - hpc3_state: state.clone(), - ioc: ioc.clone(), + // The interrupt status, and its lines: each SCSI PDMA channel's + // completion, each WD33C93A, the SEEQ. Enet channels 10/11 have no + // DMA completion line: the SEEQ drives the interrupt. + let irqs = Arc::new(Hpc3Interrupts { + state: state.clone(), + scsi: [ioc.irq_line(IocInterrupt::Scsi0), ioc.irq_line(IocInterrupt::Scsi1)], + ethernet: ioc.irq_line(IocInterrupt::Ethernet), + scsi_dma: [pdma_channels[HPC3_PDMA_CHAN_SCSI0 as usize].clone(), pdma_channels[HPC3_PDMA_CHAN_SCSI1 as usize].clone()], }); + pdma_channels[HPC3_PDMA_CHAN_SCSI0 as usize].lock().irq = Some(irqs.line(HPC3_INTSTAT_SCSI0_DMA)); + pdma_channels[HPC3_PDMA_CHAN_SCSI1 as usize].lock().irq = Some(irqs.line(HPC3_INTSTAT_SCSI1_DMA)); + let seeq_irq = irqs.line(HPC3_INTSTAT_ENET_DEV); let gateway_cfg = GatewayConfig { nfs, port_forwards, @@ -1220,12 +1209,9 @@ impl Hpc3 { let _ = enet_seeq_lock.set(seeq.clone()); let scsi0_dma = Arc::new(PdmaClientImpl { channel: pdma_channels[8].clone() }); - let scsi0_irq = Arc::new(Hpc3Irq { - state: state.clone(), ioc: ioc.clone(), bit: HPC3_INTSTAT_SCSI0_DEV, ioc_line: IocInterrupt::Scsi0, - // Pair the chip-IRQ with the SCSI0 PDMA channel so a chip-INT ack - // (kernel reads SCSI_STATUS) also drops any lingering PDMA INT. - pdma_paired: Some((pdma_channels[8].clone(), HPC3_INTSTAT_SCSI0_DMA)), - }); + // A chip-INT ack (the kernel reads SCSI_STATUS) also drops any + // lingering PDMA INT on the same line; see Hpc3Interrupts. + let scsi0_irq = irqs.line(HPC3_INTSTAT_SCSI0_DEV); let scsi_dev = Arc::new(Wd33c93a::new_with_id(Some(scsi0_dma), Some(scsi0_irq), heartbeat.clone(), scsi_deferred_int, 0)); let _ = scsi_wd_lock.set(scsi_dev.clone()); @@ -1233,10 +1219,7 @@ impl Hpc3 { // Fullhouse's second WD33C93A — see scsi1_wd_lock's doc comment above. let scsi_dev1 = if !guinness { let scsi1_dma = Arc::new(PdmaClientImpl { channel: pdma_channels[9].clone() }); - let scsi1_irq = Arc::new(Hpc3Irq { - state: state.clone(), ioc: ioc.clone(), bit: HPC3_INTSTAT_SCSI1_DEV, ioc_line: IocInterrupt::Scsi1, - pdma_paired: Some((pdma_channels[9].clone(), HPC3_INTSTAT_SCSI1_DMA)), - }); + let scsi1_irq = irqs.line(HPC3_INTSTAT_SCSI1_DEV); let dev = Arc::new(Wd33c93a::new_with_id(Some(scsi1_dma), Some(scsi1_irq), heartbeat.clone(), scsi_deferred_int, 1)); let _ = scsi1_wd_lock.set(dev.clone()); Some(dev) @@ -2086,8 +2069,8 @@ impl BusDevice for Hpc3 { let mut chan = self.pdma_channels[*ch_idx].lock(); if chan.ctrl & PDMA_CTRL_INT != 0 { chan.ctrl &= !PDMA_CTRL_INT; - if let Some(cb) = &chan.callback { - cb.set_dma_interrupt(false); + if let Some(irq) = &chan.irq { + irq.set(false); } } } @@ -2205,7 +2188,7 @@ impl Resettable for Hpc3 { state.pbus_pio = [0; 0x1000]; } // Reset all DMA channels to their power-on state. - // We preserve the configuration fields (active_mask, sys_mem, callback, etc.) + // We preserve the configuration fields (active_mask, sys_mem, irq, etc.) // that are wired at construction time, and only reset the transfer-state registers. for chan_arc in &self.pdma_channels { let mut chan = chan_arc.lock(); @@ -2328,6 +2311,53 @@ impl Saveable for Hpc3 { mod tests { use super::*; + /// INTSTAT as a controller: each SCSI channel's chip and DMA bits are + /// ORed onto its IOC line; a chip-INT ack also settles the paired PDMA + /// half; the SEEQ's level goes to Ethernet as it is. + #[test] + fn intstat_ors_scsi_halves_and_a_chip_ack_settles_the_dma_half() { + use crate::irq::LevelProbe; + let (s0, s0_line) = LevelProbe::line(); + let (s1, s1_line) = LevelProbe::line(); + let (en, en_line) = LevelProbe::line(); + let dump = Arc::new(AtomicU32::new(0)); + let chans = [ + Arc::new(Mutex::new(PdmaChannel::new(8, dump.clone()))), + Arc::new(Mutex::new(PdmaChannel::new(9, dump))), + ]; + let state = Arc::new(Mutex::new(Hpc3State { intstat: 0, gio_misc: 0, eeprom_reg: 0, pbus_pio: [0; 0x1000] })); + let irqs = Arc::new(Hpc3Interrupts { + state: state.clone(), + scsi: [s0_line, s1_line], + ethernet: en_line, + scsi_dma: chans.clone(), + }); + let (dev0, dma0) = (irqs.line(HPC3_INTSTAT_SCSI0_DEV), irqs.line(HPC3_INTSTAT_SCSI0_DMA)); + + dma0.set(true); + assert!(s0.level() && !s1.level()); + dev0.set(true); + dma0.set(false); + assert!(s0.level(), "the chip half still holds the line"); + + dma0.set(true); + chans[0].lock().ctrl |= PDMA_CTRL_INT; + dev0.set(false); + assert!(!s0.level(), "the chip ack settles both halves"); + assert_eq!(state.lock().intstat & SCSI_BITS[0], 0); + assert_eq!(chans[0].lock().ctrl & PDMA_CTRL_INT, 0); + + irqs.line(HPC3_INTSTAT_SCSI1_DMA).set(true); + assert!(s1.level() && !s0.level()); + + state.lock().intstat |= HPC3_INTSTAT_ENET_RX_DMA; + let enet = irqs.line(HPC3_INTSTAT_ENET_DEV); + enet.set(true); + assert!(en.level()); + enet.set(false); + assert!(!en.level(), "the SEEQ's own level, not ORed with the DMA bits"); + } + #[test] fn stop_saves_both_battery_backed_devices() { let dir = std::env::temp_dir().join(format!("iris-hpc3-stop-{}", std::process::id())); diff --git a/src/dev/ioc.rs b/src/dev/ioc.rs index 7cfc7705..abaddffa 100644 --- a/src/dev/ioc.rs +++ b/src/dev/ioc.rs @@ -5,11 +5,12 @@ use crate::devlog::LogModule; use std::sync::mpsc; use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, BusDevice, Device, Resettable, Saveable, MachineEvent}; use crate::snapshot::{get_field, toml_u8, hex_u8}; -use crate::dev::z85c30::{Z85c30, IrqCallback}; +use crate::dev::z85c30::Z85c30; use crate::dev::pit8254::{Pit8254, TimerCallback}; use crate::cpu::mips_core::{CAUSE_IP2, CAUSE_IP3, CAUSE_IP4, CAUSE_IP5, CAUSE_IP6}; -use crate::dev::ps2::{Ps2Controller, Ps2Callback}; +use crate::dev::ps2::Ps2Controller; use crate::hptimer::TimerManager; +use crate::irq::{InterruptController, IrqLine}; use std::io::Write; pub const IOC_BASE: u32 = 0x1FBD9800; @@ -237,6 +238,29 @@ pub enum IocInterrupt { GioS1Retrace, // EXTIO_S1_RETRACE = 0x0001, sets L1_STAT VERTICAL_RETRACE } +impl IocInterrupt { + /// Every source, in declaration order: the line numbers of the IOC's + /// [`InterruptController`] inputs. + pub const ALL: [IocInterrupt; 31] = [ + Self::Graphics, Self::Parallel, Self::McDma, Self::Ethernet, Self::Scsi1, Self::Scsi0, Self::FifoFull, + Self::VerticalRetrace, Self::VideoVsync, Self::AcFail, Self::HpcDma, Self::Gp2, Self::Panel, Self::Gp0, + Self::Serial, Self::KbMouse, Self::GioExp0, Self::GioExp1, + Self::Mappable0, Self::Mappable1, Self::Mappable2, Self::Mappable3, + Self::GioSgFifo, Self::GioSgGraphics, Self::GioSgRetrace, + Self::GioS0Fifo, Self::GioS0Graphics, Self::GioS0Retrace, + Self::GioS1Fifo, Self::GioS1Graphics, Self::GioS1Retrace, + ]; + + /// This source's line number on the IOC's controller. + pub const fn line(self) -> u32 { + self as u32 + } + + pub fn from_line(line: u32) -> Option { + Self::ALL.get(line as usize).copied() + } +} + /// The 3 GIO64 bus slots, indexed as MAME's `gio64_slot_device::slot_type_t` /// (`GIO64_SLOT_GFX`/`EXP0`/`EXP1` = 0/1/2 — see `$HOME/gits/mame/src/devices/bus/gio64/gio64.h`). /// Physically present on both Indy and Indigo2 (`indy_indigo2.cpp` instantiates @@ -426,9 +450,16 @@ struct IocState { // accessed through Arc>, which is what actually needs Send. unsafe impl Send for IocState {} -struct IocIrqLine { - state: Arc>, - source: IocInterrupt, +/// The IOC's interrupt inputs, as a controller devices hold lines into: line +/// `n` is the [`IocInterrupt`] numbered `n` (see [`IocInterrupt::line`]). +struct IocInputs(Arc>); + +impl InterruptController for IocInputs { + fn set_line(&self, line: u32, level: bool) { + if let Some(source) = IocInterrupt::from_line(line) { + self.0.lock().set_source(source, level); + } + } } struct IocTimerCallback { @@ -457,21 +488,12 @@ impl TimerCallback for IocTimerCallback { } } -impl IrqCallback for IocIrqLine { - fn set_level(&self, level: bool) { - let mut state = self.state.lock(); - match self.source { - IocInterrupt::Serial => if level { state.map_stat |= map_regs::SERIAL } else { state.map_stat &= !map_regs::SERIAL }, - IocInterrupt::KbMouse => if level { state.map_stat |= map_regs::KBD_MOUSE } else { state.map_stat &= !map_regs::KBD_MOUSE }, - _ => {} // Only Serial supported via this callback for now +/// Line `n` is the [`IocInterrupt`] numbered `n`, as through [`Ioc::irq_line`]. +impl InterruptController for Ioc { + fn set_line(&self, line: u32, level: bool) { + if let Some(source) = IocInterrupt::from_line(line) { + self.set_interrupt(source, level); } - state.update_interrupts(); - } -} - -impl Ps2Callback for IocIrqLine { - fn set_interrupt(&self, active: bool) { - self.set_level(active); } } @@ -555,10 +577,7 @@ impl Ioc { interrupts: None, })); - let serial_irq = Arc::new(IocIrqLine { - state: state.clone(), - source: IocInterrupt::Serial, - }); + let serial_irq = IrqLine::new(Arc::new(IocInputs(state.clone())), IocInterrupt::Serial.line()); let clock_ticks = Arc::new(std::sync::OnceLock::new()); let timer0_cb = Arc::new(IocTimerCallback { @@ -573,10 +592,7 @@ impl Ioc { ticks: clock_ticks.clone(), }); - let ps2_cb = Arc::new(IocIrqLine { - state: state.clone(), - source: IocInterrupt::KbMouse, - }); + let ps2_irq = IrqLine::new(Arc::new(IocInputs(state.clone())), IocInterrupt::KbMouse.line()); let scc = if ci_mode { Z85c30::new_null(Some(serial_irq)) @@ -588,7 +604,7 @@ impl Ioc { state, scc, pit: Pit8254::new(1_000_000, Some(timer0_cb), Some(timer1_cb), None), - ps2: Arc::new(Ps2Controller::new(Some(ps2_cb))), + ps2: Arc::new(Ps2Controller::new(Some(ps2_irq))), guinness, event_tx: Arc::new(std::sync::OnceLock::new()), heartbeat: Arc::new(std::sync::OnceLock::new()), @@ -651,79 +667,12 @@ impl Ioc { /// *addresses* differ by profile (PIO4 int2_map vs PIO6 base_map's /// upper half), handled in hpc3.rs's PIO dispatch — not here. pub fn set_interrupt(&self, source: IocInterrupt, active: bool) { - // Note: map_pol (polarity) register is currently ignored. - // We assume active-high logic internally for now. - match source { - IocInterrupt::VerticalRetrace | IocInterrupt::VideoVsync => {}, - _ => dlog_dev!(LogModule::Ioc, "IOC: Set Interrupt {:?} = {}", source, active), - } - let mut state = self.state.lock(); - match source { - // Local 0 - IocInterrupt::Graphics => if active { state.l0_stat |= l0_regs::GRAPHICS } else { state.l0_stat &= !l0_regs::GRAPHICS }, - IocInterrupt::Parallel => if active { state.l0_stat |= l0_regs::PARALLEL } else { state.l0_stat &= !l0_regs::PARALLEL }, - IocInterrupt::McDma => if active { state.l0_stat |= l0_regs::MC_DMA } else { state.l0_stat &= !l0_regs::MC_DMA }, - IocInterrupt::Ethernet => if active { state.l0_stat |= l0_regs::ETHERNET } else { state.l0_stat &= !l0_regs::ETHERNET }, - IocInterrupt::Scsi1 => if active { state.l0_stat |= l0_regs::SCSI1 } else { state.l0_stat &= !l0_regs::SCSI1 }, - IocInterrupt::Scsi0 => if active { state.l0_stat |= l0_regs::SCSI0 } else { state.l0_stat &= !l0_regs::SCSI0 }, - IocInterrupt::FifoFull => if active { state.l0_stat |= l0_regs::FIFO_FULL } else { state.l0_stat &= !l0_regs::FIFO_FULL }, - - // Local 1 - IocInterrupt::VerticalRetrace => if active { state.l1_stat |= l1_regs::VERTICAL_RETRACE } else { state.l1_stat &= !l1_regs::VERTICAL_RETRACE }, - IocInterrupt::VideoVsync => if active { state.l1_stat |= l1_regs::VIDEO_VSYNC } else { state.l1_stat &= !l1_regs::VIDEO_VSYNC }, - IocInterrupt::AcFail => if active { state.l1_stat |= l1_regs::AC_FAIL } else { state.l1_stat &= !l1_regs::AC_FAIL }, - IocInterrupt::HpcDma => if active { state.l1_stat |= l1_regs::HPC_DMA } else { state.l1_stat &= !l1_regs::HPC_DMA }, - IocInterrupt::Gp2 => if active { state.l1_stat |= l1_regs::GP2 } else { state.l1_stat &= !l1_regs::GP2 }, - IocInterrupt::Panel => if active { state.l1_stat |= l1_regs::PANEL } else { state.l1_stat &= !l1_regs::PANEL }, - IocInterrupt::Gp0 => if active { state.l1_stat |= l1_regs::GP0 } else { state.l1_stat &= !l1_regs::GP0 }, - - // Mappable (LIO_2 on IP24) - IocInterrupt::Serial => if active { state.map_stat |= map_regs::SERIAL } else { state.map_stat &= !map_regs::SERIAL }, - IocInterrupt::KbMouse => if active { state.map_stat |= map_regs::KBD_MOUSE } else { state.map_stat &= !map_regs::KBD_MOUSE }, - IocInterrupt::GioExp0 => if active { state.map_stat |= map_regs::GIO_EXP0 } else { state.map_stat &= !map_regs::GIO_EXP0 }, - IocInterrupt::GioExp1 => if active { state.map_stat |= map_regs::GIO_EXP1 } else { state.map_stat &= !map_regs::GIO_EXP1 }, - IocInterrupt::Mappable0 => if active { state.map_stat |= 1 << 0 } else { state.map_stat &= !(1 << 0) }, - IocInterrupt::Mappable1 => if active { state.map_stat |= 1 << 1 } else { state.map_stat &= !(1 << 1) }, - IocInterrupt::Mappable2 => if active { state.map_stat |= 1 << 2 } else { state.map_stat &= !(1 << 2) }, - IocInterrupt::Mappable3 => if active { state.map_stat |= 1 << 3 } else { state.map_stat &= !(1 << 3) }, + self.state.lock().set_source(source, active); + } - // Fullhouse GIO slot fan-out (see GIO_SLOT_MAP's doc comment): - // each sets the one shared L0/L1 bit every slot's signal uses, - // and clears (active) / sets (idle) its own active-low EXT_IO - // bit so ip22_gio0/1/2_intr can tell which slot fired. - IocInterrupt::GioSgFifo => { - if active { state.l0_stat |= l0_regs::FIFO_FULL; state.ext_io &= !ext_io_regs::SG_IRQ_2; } - else { state.l0_stat &= !l0_regs::FIFO_FULL; state.ext_io |= ext_io_regs::SG_IRQ_2; } - } - IocInterrupt::GioSgGraphics => { - if active { state.l0_stat |= l0_regs::GRAPHICS; state.ext_io &= !ext_io_regs::SG_IRQ_1; } - else { state.l0_stat &= !l0_regs::GRAPHICS; state.ext_io |= ext_io_regs::SG_IRQ_1; } - } - IocInterrupt::GioSgRetrace => { - if active { state.l1_stat |= l1_regs::VERTICAL_RETRACE; state.ext_io &= !ext_io_regs::SG_RETRACE; } - else { state.l1_stat &= !l1_regs::VERTICAL_RETRACE; state.ext_io |= ext_io_regs::SG_RETRACE; } - } - IocInterrupt::GioS0Fifo => { - if active { state.l0_stat |= l0_regs::FIFO_FULL; state.ext_io &= !ext_io_regs::S0_IRQ_2; } - else { state.l0_stat &= !l0_regs::FIFO_FULL; state.ext_io |= ext_io_regs::S0_IRQ_2; } - } - IocInterrupt::GioS0Graphics => { - if active { state.l0_stat |= l0_regs::GRAPHICS; state.ext_io &= !ext_io_regs::S0_IRQ_1; } - else { state.l0_stat &= !l0_regs::GRAPHICS; state.ext_io |= ext_io_regs::S0_IRQ_1; } - } - IocInterrupt::GioS0Retrace => { - if active { state.l1_stat |= l1_regs::VERTICAL_RETRACE; state.ext_io &= !ext_io_regs::S0_RETRACE; } - else { state.l1_stat &= !l1_regs::VERTICAL_RETRACE; state.ext_io |= ext_io_regs::S0_RETRACE; } - } - // GioS1* bits have no second EXT_IO register modeled (see - // IocState.ext_io's doc comment) — still set the shared L0/L1 - // bit so callers routing through this slot don't silently drop - // the interrupt, but the EXT_IO side is a no-op. - IocInterrupt::GioS1Fifo => if active { state.l0_stat |= l0_regs::FIFO_FULL } else { state.l0_stat &= !l0_regs::FIFO_FULL }, - IocInterrupt::GioS1Graphics => if active { state.l0_stat |= l0_regs::GRAPHICS } else { state.l0_stat &= !l0_regs::GRAPHICS }, - IocInterrupt::GioS1Retrace => if active { state.l1_stat |= l1_regs::VERTICAL_RETRACE } else { state.l1_stat &= !l1_regs::VERTICAL_RETRACE }, - } - state.update_interrupts(); + /// An [`IrqLine`] into one of the IOC's sources, for a device to drive. + pub fn irq_line(&self, source: IocInterrupt) -> IrqLine { + IrqLine::new(Arc::new(IocInputs(self.state.clone())), source.line()) } pub fn ps2(&self) -> Arc { @@ -1202,6 +1151,83 @@ impl IocState { self.map_stat &= !(val & 0x3); } + /// Drive one source's status bit (and, for the fullhouse GIO slots, + /// its EXT_IO bit), then recompute the CPU lines. + fn set_source(&mut self, source: IocInterrupt, active: bool) { + // Note: map_pol (polarity) register is currently ignored. + // We assume active-high logic internally for now. + match source { + IocInterrupt::VerticalRetrace | IocInterrupt::VideoVsync => {}, + _ => dlog_dev!(LogModule::Ioc, "IOC: Set Interrupt {:?} = {}", source, active), + } + match source { + // Local 0 + IocInterrupt::Graphics => if active { self.l0_stat |= l0_regs::GRAPHICS } else { self.l0_stat &= !l0_regs::GRAPHICS }, + IocInterrupt::Parallel => if active { self.l0_stat |= l0_regs::PARALLEL } else { self.l0_stat &= !l0_regs::PARALLEL }, + IocInterrupt::McDma => if active { self.l0_stat |= l0_regs::MC_DMA } else { self.l0_stat &= !l0_regs::MC_DMA }, + IocInterrupt::Ethernet => if active { self.l0_stat |= l0_regs::ETHERNET } else { self.l0_stat &= !l0_regs::ETHERNET }, + IocInterrupt::Scsi1 => if active { self.l0_stat |= l0_regs::SCSI1 } else { self.l0_stat &= !l0_regs::SCSI1 }, + IocInterrupt::Scsi0 => if active { self.l0_stat |= l0_regs::SCSI0 } else { self.l0_stat &= !l0_regs::SCSI0 }, + IocInterrupt::FifoFull => if active { self.l0_stat |= l0_regs::FIFO_FULL } else { self.l0_stat &= !l0_regs::FIFO_FULL }, + + // Local 1 + IocInterrupt::VerticalRetrace => if active { self.l1_stat |= l1_regs::VERTICAL_RETRACE } else { self.l1_stat &= !l1_regs::VERTICAL_RETRACE }, + IocInterrupt::VideoVsync => if active { self.l1_stat |= l1_regs::VIDEO_VSYNC } else { self.l1_stat &= !l1_regs::VIDEO_VSYNC }, + IocInterrupt::AcFail => if active { self.l1_stat |= l1_regs::AC_FAIL } else { self.l1_stat &= !l1_regs::AC_FAIL }, + IocInterrupt::HpcDma => if active { self.l1_stat |= l1_regs::HPC_DMA } else { self.l1_stat &= !l1_regs::HPC_DMA }, + IocInterrupt::Gp2 => if active { self.l1_stat |= l1_regs::GP2 } else { self.l1_stat &= !l1_regs::GP2 }, + IocInterrupt::Panel => if active { self.l1_stat |= l1_regs::PANEL } else { self.l1_stat &= !l1_regs::PANEL }, + IocInterrupt::Gp0 => if active { self.l1_stat |= l1_regs::GP0 } else { self.l1_stat &= !l1_regs::GP0 }, + + // Mappable (LIO_2 on IP24) + IocInterrupt::Serial => if active { self.map_stat |= map_regs::SERIAL } else { self.map_stat &= !map_regs::SERIAL }, + IocInterrupt::KbMouse => if active { self.map_stat |= map_regs::KBD_MOUSE } else { self.map_stat &= !map_regs::KBD_MOUSE }, + IocInterrupt::GioExp0 => if active { self.map_stat |= map_regs::GIO_EXP0 } else { self.map_stat &= !map_regs::GIO_EXP0 }, + IocInterrupt::GioExp1 => if active { self.map_stat |= map_regs::GIO_EXP1 } else { self.map_stat &= !map_regs::GIO_EXP1 }, + IocInterrupt::Mappable0 => if active { self.map_stat |= 1 << 0 } else { self.map_stat &= !(1 << 0) }, + IocInterrupt::Mappable1 => if active { self.map_stat |= 1 << 1 } else { self.map_stat &= !(1 << 1) }, + IocInterrupt::Mappable2 => if active { self.map_stat |= 1 << 2 } else { self.map_stat &= !(1 << 2) }, + IocInterrupt::Mappable3 => if active { self.map_stat |= 1 << 3 } else { self.map_stat &= !(1 << 3) }, + + // Fullhouse GIO slot fan-out (see GIO_SLOT_MAP's doc comment): + // each sets the one shared L0/L1 bit every slot's signal uses, + // and clears (active) / sets (idle) its own active-low EXT_IO + // bit so ip22_gio0/1/2_intr can tell which slot fired. + IocInterrupt::GioSgFifo => { + if active { self.l0_stat |= l0_regs::FIFO_FULL; self.ext_io &= !ext_io_regs::SG_IRQ_2; } + else { self.l0_stat &= !l0_regs::FIFO_FULL; self.ext_io |= ext_io_regs::SG_IRQ_2; } + } + IocInterrupt::GioSgGraphics => { + if active { self.l0_stat |= l0_regs::GRAPHICS; self.ext_io &= !ext_io_regs::SG_IRQ_1; } + else { self.l0_stat &= !l0_regs::GRAPHICS; self.ext_io |= ext_io_regs::SG_IRQ_1; } + } + IocInterrupt::GioSgRetrace => { + if active { self.l1_stat |= l1_regs::VERTICAL_RETRACE; self.ext_io &= !ext_io_regs::SG_RETRACE; } + else { self.l1_stat &= !l1_regs::VERTICAL_RETRACE; self.ext_io |= ext_io_regs::SG_RETRACE; } + } + IocInterrupt::GioS0Fifo => { + if active { self.l0_stat |= l0_regs::FIFO_FULL; self.ext_io &= !ext_io_regs::S0_IRQ_2; } + else { self.l0_stat &= !l0_regs::FIFO_FULL; self.ext_io |= ext_io_regs::S0_IRQ_2; } + } + IocInterrupt::GioS0Graphics => { + if active { self.l0_stat |= l0_regs::GRAPHICS; self.ext_io &= !ext_io_regs::S0_IRQ_1; } + else { self.l0_stat &= !l0_regs::GRAPHICS; self.ext_io |= ext_io_regs::S0_IRQ_1; } + } + IocInterrupt::GioS0Retrace => { + if active { self.l1_stat |= l1_regs::VERTICAL_RETRACE; self.ext_io &= !ext_io_regs::S0_RETRACE; } + else { self.l1_stat &= !l1_regs::VERTICAL_RETRACE; self.ext_io |= ext_io_regs::S0_RETRACE; } + } + // GioS1* bits have no second EXT_IO register modeled (see + // IocState.ext_io's doc comment) — still set the shared L0/L1 + // bit so callers routing through this slot don't silently drop + // the interrupt, but the EXT_IO side is a no-op. + IocInterrupt::GioS1Fifo => if active { self.l0_stat |= l0_regs::FIFO_FULL } else { self.l0_stat &= !l0_regs::FIFO_FULL }, + IocInterrupt::GioS1Graphics => if active { self.l0_stat |= l0_regs::GRAPHICS } else { self.l0_stat &= !l0_regs::GRAPHICS }, + IocInterrupt::GioS1Retrace => if active { self.l1_stat |= l1_regs::VERTICAL_RETRACE } else { self.l1_stat &= !l1_regs::VERTICAL_RETRACE }, + } + self.update_interrupts(); + } + fn update_interrupts(&mut self) { // 1. Update Mappable Interrupts (MAP_INT0, MAP_INT1) let map_int0 = (self.map_stat & self.map_mask0) != 0; @@ -1343,6 +1369,28 @@ impl Saveable for Ioc { mod tests { use super::*; + /// Every source has its own line, and a line drives exactly what + /// `set_interrupt` with that source does. + #[test] + fn each_source_is_a_line_on_the_ioc() { + for (n, src) in IocInterrupt::ALL.iter().enumerate() { + assert_eq!(src.line(), n as u32, "{src:?} out of declaration order"); + assert_eq!(IocInterrupt::from_line(n as u32), Some(*src)); + } + assert_eq!(IocInterrupt::from_line(IocInterrupt::ALL.len() as u32), None); + + let ioc = Ioc::new_ci(false); + let serial = ioc.irq_line(IocInterrupt::Serial); + serial.set(true); + assert_ne!(ioc.state.lock().map_stat & map_regs::SERIAL, 0); + serial.set(false); + assert_eq!(ioc.state.lock().map_stat & map_regs::SERIAL, 0); + ioc.irq_line(IocInterrupt::Scsi0).set(true); + assert_ne!(ioc.state.lock().l0_stat & l0_regs::SCSI0, 0); + InterruptController::set_line(&ioc, IocInterrupt::Scsi0.line(), false); + assert_eq!(ioc.state.lock().l0_stat & l0_regs::SCSI0, 0); + } + /// Timer 1 feeds the status bar's Hz, timer 0 does not; a tick landing on /// a pending one is merged; TMR_CLR on a set bit is an acknowledgement and /// on a clear bit is not. diff --git a/src/dev/mc.rs b/src/dev/mc.rs index 147fe91c..b016bf62 100644 --- a/src/dev/mc.rs +++ b/src/dev/mc.rs @@ -7,7 +7,7 @@ use std::thread; use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, BusDevice, Device, Signal, Resettable, Saveable, MachineEvent}; use crate::snapshot::{get_field, u32_slice_to_toml, load_u32_slice, toml_u32, toml_bool, hex_u32}; use crate::dev::eeprom_93c56::Eeprom93c56; -use crate::dev::ioc::{Ioc, IocInterrupt}; +use crate::irq::IrqLine; use std::io::Write as IoWrite; // The GIO virtual DMA engine — registers, µTLB translation and the transfer @@ -65,7 +65,8 @@ struct MemoryControllerState { cpu_cycle_acc: u64, rpss_cycle_acc: u64, cpu: Option>, - ioc: Option, + /// The GIO DMA engine's interrupt (the IOC's MC DMA line). + dma_irq: Option, } #[derive(Clone)] @@ -134,7 +135,7 @@ impl MemoryController { cpu_cycle_acc: 0, rpss_cycle_acc: 0, cpu: None, - ioc: None, + dma_irq: None, })), giodma: Arc::new(GioDma::new()), phys: Arc::new(OnceLock::new()), @@ -243,8 +244,9 @@ impl MemoryController { self.state.lock().cpu = Some(cpu); } - pub fn set_ioc(&self, ioc: Ioc) { - self.state.lock().ioc = Some(ioc); + /// Wire the GIO DMA engine's interrupt. + pub fn set_dma_irq(&self, irq: IrqLine) { + self.state.lock().dma_irq = Some(irq); } pub fn set_phys(&self, phys: Arc) { @@ -527,7 +529,7 @@ impl MemoryController { /// The IOC handle, cloned out so the caller does not hold the MC state lock /// while raising an interrupt. - pub(crate) fn ioc(&self) -> Option { self.state.lock().ioc.clone() } + pub(crate) fn dma_irq(&self) -> Option { self.state.lock().dma_irq.clone() } /// Current GIO64_ARB value (bus widths / arbitration per GIO slot). pub(crate) fn gio64_arb(&self) -> u32 { self.state.lock().regs[(REG_GIO64_ARB / 4) as usize] } @@ -877,8 +879,8 @@ impl BusDevice for MemoryController { dma.run = (dma.run & !0xF) | (val & 0xF); drop(dma); if val == 0 { - if let Some(ioc) = &state.ioc { - ioc.set_interrupt(IocInterrupt::McDma, false); + if let Some(irq) = &state.dma_irq { + irq.set(false); } } BUS_OK diff --git a/src/dev/mc_vdma.rs b/src/dev/mc_vdma.rs index 8d27d7c6..f90c737f 100644 --- a/src/dev/mc_vdma.rs +++ b/src/dev/mc_vdma.rs @@ -69,7 +69,6 @@ use std::io::Write as IoWrite; use crate::devlog::LogModule; use crate::traits::BUS_BUSY; -use crate::dev::ioc::IocInterrupt; use crate::dev::mc::MemoryController; // ── DMA register offsets (MC base 0x1FA00000) ─────────────────────────────── @@ -411,8 +410,8 @@ pub struct VdmaResult { impl MemoryController { pub(crate) fn signal_dma_interrupt(&self) { - if let Some(ioc) = self.ioc() { - ioc.set_interrupt(IocInterrupt::McDma, true); + if let Some(irq) = self.dma_irq() { + irq.set(true); } } diff --git a/src/dev/mgras/mod.rs b/src/dev/mgras/mod.rs index 77589bce..8e13000e 100644 --- a/src/dev/mgras/mod.rs +++ b/src/dev/mgras/mod.rs @@ -264,7 +264,9 @@ pub struct Mgras { /// a full FIFO. pf_wait: crate::PfLevel, pf_fifo_full: crate::PfLevel, - ioc: crate::dev::ioc::Ioc, + /// The board's three GIO interrupts, as the slot wires them: FIFO, + /// general, retrace (see [`Line`]). + irqs: [crate::irq::IrqLine; 3], /// Current level of the general interrupt line. general: AtomicBool, /// The FIFO interrupt's inputs, kept apart from `front` for the HQ3 @@ -456,14 +458,16 @@ impl Mgras { /// Build the board in place on the heap: its multi-megabyte state is /// zero-initialised without touching the stack, and only the fields that /// are not valid zeroed are written. - pub fn new(board: GraphicsBoard, ioc: crate::dev::ioc::Ioc, heartbeat: Arc, fasttick: Arc) -> Arc { + /// `irqs` are the FIFO, general and retrace interrupts, as the slot + /// wires them. + pub fn new(board: GraphicsBoard, irqs: [crate::irq::IrqLine; 3], heartbeat: Arc, fasttick: Arc) -> Arc { let mut a: Arc> = Arc::new_zeroed(); let p = Arc::get_mut(&mut a).unwrap().as_mut_ptr(); // SAFETY: `p` points at zeroed, exclusively owned memory. Every field // that is not valid zeroed is written below, before assume_init. let m = unsafe { addr_of_mut!((*p).kind).write(board); - addr_of_mut!((*p).ioc).write(ioc); + addr_of_mut!((*p).irqs).write(irqs); addr_of_mut!((*p).general).write(AtomicBool::new(false)); addr_of_mut!((*p).fifo_cfg).write(AtomicU32::new(0)); addr_of_mut!((*p).fifo_line).write(SpinLock::new(false)); @@ -852,15 +856,14 @@ impl Mgras { *self.mem.lock() = Some(mem); } - /// Drive one of the board's interrupt lines (graphics slot wiring). + /// Drive one of the board's interrupt lines. fn set_line(&self, line: Line, active: bool) { - use crate::dev::ioc::IocInterrupt; - let src = match line { - Line::Fifo => IocInterrupt::GioSgFifo, - Line::General => IocInterrupt::GioSgGraphics, - Line::Retrace => IocInterrupt::GioSgRetrace, + let n = match line { + Line::Fifo => 0, + Line::General => 1, + Line::Retrace => 2, }; - self.ioc.set_interrupt(src, active); + self.irqs[n].set(active); } /// Wire up the CPU cycle counter for the status bar's MIPS figure. diff --git a/src/dev/mgras/record.rs b/src/dev/mgras/record.rs index 83eb98fe..6fc6cedb 100644 --- a/src/dev/mgras/record.rs +++ b/src/dev/mgras/record.rs @@ -305,7 +305,7 @@ pub(super) mod tests { /// A board of kind `kind` with its engine threads running (no display). pub fn live_board_of(kind: GraphicsBoard) -> Arc { let hb = Arc::new(std::sync::atomic::AtomicU64::new(0)); - let m = Mgras::new(kind, crate::dev::ioc::Ioc::new(false), hb.clone(), hb); + let m = Mgras::new(kind, std::array::from_fn(|_| crate::irq::IrqLine::unconnected()), hb.clone(), hb); m.start_engines(); m } diff --git a/src/dev/ng1/rex3.rs b/src/dev/ng1/rex3.rs index 26e8bba9..9a043e0f 100644 --- a/src/dev/ng1/rex3.rs +++ b/src/dev/ng1/rex3.rs @@ -1354,11 +1354,11 @@ pub struct Rex3 { fb_dirty: AtomicBool, pub screen: Arc>, /// Drives the real REX3 `VV_INT_N` pin (vertical retrace / Kaleidoscope). - pub vblank_cb: Mutex>>, + pub vblank_cb: Mutex>, /// Drives the real REX3 `FIFO_INT_N` pin (GFIFO/BFIFO above/below /// interrupt — one physical pin for both directions, selected by /// `CONFIG_GFIFOABOVEINT`; see `update_gfifo_irqs`). - pub fifo_full_cb: Mutex>>, + pub fifo_full_cb: Mutex>, /// Perfetto edge trackers: guest sees GFXBUSY, CPU stalled on a full /// GFIFO, GFIFO level as STATUS reports it. pf_wait: crate::PfLevel, @@ -1671,12 +1671,14 @@ impl Rex3 { pub const DIAG_LOOP_DRAW_BLOCK: u64 = 1 << 24; pub const DIAG_LOOP_EXECUTE_GO: u64 = 1 << 25; - pub fn set_vblank_callback(&self, cb: Arc) { - *self.vblank_cb.lock() = Some(cb); + /// Wire `VV_INT_N` (vertical retrace) to `irq`. + pub fn set_vblank_irq(&self, irq: crate::irq::IrqLine) { + *self.vblank_cb.lock() = Some(irq); } - pub fn set_fifo_full_callback(&self, cb: Arc) { - *self.fifo_full_cb.lock() = Some(cb); + /// Wire `FIFO_INT_N` to `irq`. + pub fn set_fifo_full_irq(&self, irq: crate::irq::IrqLine) { + *self.fifo_full_cb.lock() = Some(irq); } /// Program VC2 with a host-side Newport timing preset (see `[graphics] resolution`). @@ -1732,8 +1734,8 @@ impl Rex3 { self.config.status.fetch_and(!STATUS_GFIFO_INT, Ordering::Relaxed); } - if let Some(cb) = self.fifo_full_cb.lock().clone() { - cb(crossed); + if let Some(irq) = self.fifo_full_cb.lock().clone() { + irq.set(crossed); } } @@ -3109,9 +3111,12 @@ impl Rex3 { } { self.diag.fetch_or(Self::DIAG_LOCK_VBLANK_CB, Ordering::Relaxed); - let cb = self.vblank_cb.lock().clone(); + let irq = self.vblank_cb.lock().clone(); self.diag.fetch_and(!Self::DIAG_LOCK_VBLANK_CB, Ordering::Relaxed); - if let Some(cb) = cb { cb(true); } + if let Some(irq) = irq { + crate::pf_instant!(crate::perfetto::events::REX3_VBLANK); + irq.set(true); + } } // Present: compositor → overlay → status bar → swap @@ -3169,9 +3174,12 @@ impl Rex3 { } { self.diag.fetch_or(Self::DIAG_LOCK_VBLANK_CB, Ordering::Relaxed); - let cb = self.vblank_cb.lock().clone(); + let irq = self.vblank_cb.lock().clone(); self.diag.fetch_and(!Self::DIAG_LOCK_VBLANK_CB, Ordering::Relaxed); - if let Some(cb) = cb { cb(true); } + if let Some(irq) = irq { + crate::pf_instant!(crate::perfetto::events::REX3_VBLANK); + irq.set(true); + } } } @@ -4138,13 +4146,13 @@ impl Rex3 { if reg_offset == REX3_STATUS { let had_vrint = self.config.status.fetch_and(!STATUS_VRINT, Ordering::Relaxed) & STATUS_VRINT != 0; if had_vrint { - let cb = self.vblank_cb.lock().clone(); - if let Some(cb) = cb { cb(false); } + let irq = self.vblank_cb.lock().clone(); + if let Some(irq) = irq { irq.set(false); } } let had_gfifo = self.config.status.fetch_and(!STATUS_GFIFO_INT, Ordering::Relaxed) & STATUS_GFIFO_INT != 0; if had_gfifo { - let cb = self.fifo_full_cb.lock().clone(); - if let Some(cb) = cb { cb(false); } + let irq = self.fifo_full_cb.lock().clone(); + if let Some(irq) = irq { irq.set(false); } } let dcb = self.dcb.lock(); if let Some(until) = dcb.backbusy_until { diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index a8e6648b..b4243a1c 100644 --- a/src/dev/ps2.rs +++ b/src/dev/ps2.rs @@ -8,12 +8,7 @@ use crate::traits::{Device, Resettable, Saveable}; use crate::snapshot::{get_field, toml_u8, toml_bool, hex_u8}; use crate::devlog::LogModule; use crate::config::KeyboardLayout; - -/// Callback trait for PS/2 interrupts -pub trait Ps2Callback: Send + Sync { - /// Set the interrupt line state - fn set_interrupt(&self, active: bool); -} +use crate::irq::IrqLine; enum Ps2Source { Keyboard, @@ -56,7 +51,8 @@ struct Ps2State { /// Combined PS/2 Keyboard and Mouse Controller pub struct Ps2Controller { state: Mutex, - callback: Option>, + /// The controller's interrupt: up while a byte waits to be read. + irq: Option, running: AtomicBool, /// Host keyboard shape (`KeyboardLayout as u8`). Host config, not guest /// state, so it is not in snapshots. @@ -65,7 +61,7 @@ pub struct Ps2Controller { impl Ps2Controller { /// Create a new PS/2 Controller - pub fn new(callback: Option>) -> Self { + pub fn new(irq: Option) -> Self { Self { state: Mutex::new(Ps2State { rx_queue: VecDeque::new(), @@ -83,7 +79,7 @@ impl Ps2Controller { key_modes: [3; 256], held_keys: [false; 256], }), - callback, + irq, running: AtomicBool::new(false), layout: AtomicU8::new(KeyboardLayout::Ansi as u8), } @@ -96,11 +92,11 @@ impl Ps2Controller { /// Update interrupt state based on queue status fn update_interrupt(&self) { - if let Some(cb) = &self.callback { + if let Some(line) = &self.irq { let state = self.state.lock(); let len = state.rx_queue.len(); drop(state); - cb.set_interrupt(len > 0); + line.set(len > 0); } } @@ -1254,6 +1250,18 @@ impl Saveable for Ps2Controller { mod tests { use super::*; + /// The interrupt is up exactly while a byte waits to be read. + #[test] + fn the_irq_line_follows_the_receive_queue() { + let (probe, line) = crate::irq::LevelProbe::line(); + let ps2 = Ps2Controller::new(Some(line)); + ps2.start(); + ps2.write_data(0xF4); // enable scanning: answered with an ACK + assert!(probe.level(), "the ACK is waiting"); + assert_eq!(ps2.read_data(), 0xFA); + assert!(!probe.level(), "and once read, the line drops"); + } + #[test] fn set3_make_break_suppresses_host_repeat_and_preserves_release() { let ps2 = Ps2Controller::new(None); diff --git a/src/dev/seeq8003.rs b/src/dev/seeq8003.rs index 9a442912..ae75f3a8 100644 --- a/src/dev/seeq8003.rs +++ b/src/dev/seeq8003.rs @@ -10,6 +10,7 @@ // Register model: the SEEQ status registers (rx_stat/tx_stat) hold device-level // OLD/NEW status for interrupt acknowledgement only. +use crate::irq::IrqLine; use std::io::Write; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; @@ -144,12 +145,6 @@ pub struct SeeqState { pub ts: u32, } -// ── Interrupt callback ──────────────────────────────────────────────────────── -pub trait SeeqCallback: Send + Sync { - /// Raise or lower the interrupt line. Called by Seeq8003 with SeeqState lock held. - /// Implementor must not re-acquire SeeqState lock. - fn set_interrupt(&self, level: bool); -} // ── Ring-buffer capacity (number of frames) ─────────────────────────────────── const CHAN_CAPACITY: usize = 256; @@ -164,7 +159,9 @@ const RX_DRAIN_CAP: usize = 256; // ── Main device struct ──────────────────────────────────────────────────────── pub struct Seeq8003 { state: Arc>, - callback: Option>, + /// The interrupt line. Driven with SeeqState's lock held, so whatever it + /// leads into must not take that lock again. + irq: Option, rx_dma: Option>, tx_dma: Option>, sys_mem: Mutex>>, @@ -211,14 +208,14 @@ enum RxPumpResult { // ── Seeq8003 ───────────────────────────────────────────────────────────────── impl Seeq8003 { - pub fn new(callback: Option>, + pub fn new(irq: Option, rx_dma: Option>, tx_dma: Option>, heartbeat: Arc) -> Self { - Self::with_config(callback, rx_dma, tx_dma, GatewayConfig::default(), heartbeat) + Self::with_config(irq, rx_dma, tx_dma, GatewayConfig::default(), heartbeat) } - pub fn with_config(callback: Option>, + pub fn with_config(irq: Option, rx_dma: Option>, tx_dma: Option>, config: GatewayConfig, @@ -242,7 +239,7 @@ impl Seeq8003 { rx_nothing: 0, ts: 0, })), - callback, rx_dma, tx_dma, + irq, rx_dma, tx_dma, sys_mem: Mutex::new(None), in_reset: Arc::new(AtomicBool::new(false)), config, @@ -258,7 +255,7 @@ impl Seeq8003 { } } - fn raise_interrupt(state: &mut SeeqState, dma_irq: bool, callback: &Option>) { + fn raise_interrupt(state: &mut SeeqState, dma_irq: bool, irq: &Option) { // Interrupt when status is NEW (OLD=0) and the corresponding IRQ enable bit is set. let rx_irq = (state.rx_stat & rx_stat::OLD) == 0 && { let enabled = state.rx_cmd & 0x1f; @@ -274,10 +271,10 @@ impl Seeq8003 { let level = tx_irq || rx_irq || dma_irq; if level && !state.intpend { state.intpend = true; - if let Some(cb) = callback { cb.set_interrupt(true); } + if let Some(irq) = irq { irq.set(true); } } else if !level && state.intpend { state.intpend = false; - if let Some(cb) = callback { cb.set_interrupt(false); } + if let Some(irq) = irq { irq.set(false); } } } @@ -301,7 +298,7 @@ impl Seeq8003 { st.tx_stat |= tx_stat::OLD; st.intpend = false; dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=reset_interrupt(CLRINT) rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - if let Some(ref cb) = self.callback { cb.set_interrupt(false); } + if let Some(ref irq) = self.irq { irq.set(false); } } /// Returns true if an unacknowledged interrupt is pending. @@ -456,7 +453,7 @@ impl Seeq8003 { st.tx_cmd = 0; st.ctl = 0; dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=deassert_reset rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, false, &self.callback); + Self::raise_interrupt(&mut st, false, &self.irq); } /// Immediate reset (legacy): assert then immediately deassert. @@ -481,14 +478,14 @@ impl Seeq8003 { let v = st.rx_stat; st.rx_stat |= rx_stat::OLD; // mark as read / stale dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=read_rx_reg rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, false, &self.callback); + Self::raise_interrupt(&mut st, false, &self.irq); v } SEEQ_TX_REG => { let v = st.tx_stat; st.tx_stat |= tx_stat::OLD; dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=read_tx_reg rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, false, &self.callback); + Self::raise_interrupt(&mut st, false, &self.irq); v } _ => 0, @@ -529,12 +526,12 @@ impl Seeq8003 { SEEQ_RX_REG => { st.rx_cmd = val; dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=write_rx_reg rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, false, &self.callback); + Self::raise_interrupt(&mut st, false, &self.irq); } SEEQ_TX_REG => { st.tx_cmd = val; dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=write_tx_reg rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, false, &self.callback); + Self::raise_interrupt(&mut st, false, &self.irq); } _ => {} } @@ -628,7 +625,7 @@ impl Device for Seeq8003 { let rx_dma = self.rx_dma.clone(); let sys_mem_enet: Option> = self.sys_mem.lock().clone(); let state_enet = self.state.clone(); - let callback = self.callback.clone(); + let irq = self.irq.clone(); let rx_wake_enet = self.rx_wake.clone(); let tx_wake_enet = self.tx_wake.clone(); let heartbeat_enet = self.heartbeat.clone(); @@ -733,7 +730,7 @@ impl Device for Seeq8003 { busy = tx_moved || rx_moved; if did_something { dlog_dev!(LogModule::Seeq, "[ts={}] raise_interrupt caller=enet_thread dma_irq={} rx_stat={:02x} rx_cmd={:02x} tx_stat={:02x} tx_cmd={:02x}", st.ts, dma_irq, st.rx_stat, st.rx_cmd, st.tx_stat, st.tx_cmd); - Self::raise_interrupt(&mut st, dma_irq, &callback); + Self::raise_interrupt(&mut st, dma_irq, &irq); } } }).expect("seeq-enet spawn"); diff --git a/src/dev/ultra64.rs b/src/dev/ultra64.rs index d51d701c..5b9f6645 100644 --- a/src/dev/ultra64.rs +++ b/src/dev/ultra64.rs @@ -22,7 +22,7 @@ mod imp { use crate::devlog::{devlog, devlog_is_active, LogModule}; -use crate::dev::ioc::{Ioc, IocInterrupt}; +use crate::irq::IrqLine; use crate::traits::{BusDevice, BusRead32, BusRead64, BusRead16, BusRead8, BUS_OK, Device}; use crate::ultra_proto::{h2n, n2h, rdb_type, rdb_bytes, IpcRing, ShmHeader, SHM_MAGIC, SHM_VERSION, EVENT_AREA_SIZE, SHM_HEADER_OFFSET, SHM_RAMROM_OFFSET, @@ -78,7 +78,8 @@ struct Ultra64State { pub struct Ultra64 { state: Arc>, - ioc: Ioc, + /// The board's interrupt: GIO expansion slot 0 on an Indy. + irq: IrqLine, shmem: shared_memory::Shmem, // raw pointers into shmem, valid for its lifetime hdr_ptr: *mut ShmHeader, @@ -95,7 +96,7 @@ unsafe impl Send for Ultra64 {} unsafe impl Sync for Ultra64 {} impl Ultra64 { - pub fn new(ioc: Ioc) -> Result { + pub fn new(irq: IrqLine) -> Result { // Unlink any stale shm left by a previous crash before creating fresh. // This is safe: the mapping only matters while iris is running, and if // gopher64 is already attached it will notice iris restarted via magic check. @@ -143,7 +144,7 @@ impl Ultra64 { })); Ok(Ultra64 { - state, ioc, shmem, hdr_ptr, ramrom_ptr, evt_h2n, evt_n2h, + state, irq, shmem, hdr_ptr, ramrom_ptr, evt_h2n, evt_n2h, running: Arc::new(AtomicBool::new(false)), threads: Mutex::new(Vec::new()), }) @@ -168,7 +169,7 @@ impl Device for Ultra64 { } let state = Arc::clone(&self.state); - let ioc = self.ioc.clone(); + let irq = self.irq.clone(); let evt_n2h = Arc::clone(&self.evt_n2h); let running = Arc::clone(&self.running); let hdr_ptr = self.hdr_ptr as usize; @@ -192,7 +193,7 @@ impl Device for Ultra64 { st.gio_int_pending = true; st.gio_int_payload = payload & 0x3F; drop(st); - ioc.set_interrupt(IocInterrupt::GioExp0, true); + irq.set(true); } n2h::GIO_SYNC => { dlog!(LogModule::Ultra, "GIO_SYNC from N64: val={:#04x}", payload); @@ -204,7 +205,7 @@ impl Device for Ultra64 { // so u64_giointr()'s bit-30 path calls send_write_buffer(). dlog!(LogModule::Ultra, "RDB_READ ACK from N64 → set rdb_r_int, assert GioExp0"); state.lock().rdb_r_int = true; - ioc.set_interrupt(IocInterrupt::GioExp0, true); + irq.set(true); } n2h::RDB_WRITE => { // N64 wrote a packet to GIO_RDB_BASE_REG. @@ -220,7 +221,7 @@ impl Device for Ultra64 { st.rdb_n2h = payload; st.rdb_w_int = true; drop(st); - ioc.set_interrupt(IocInterrupt::GioExp0, true); + irq.set(true); } _ => { dlog!(LogModule::Ultra, "n2h unknown kind={} payload={:#010x}", kind, payload); @@ -483,7 +484,7 @@ impl BusDevice for Ultra64 { let payload = st.gio_int_payload & 0x3F; st.gio_int_pending = false; drop(st); - self.ioc.set_interrupt(IocInterrupt::GioExp0, false); + self.irq.set(false); dlog!(LogModule::Ultra, "rd GIO_INT_ACK @+0xC00 → {:#04x}", payload); BusRead32::ok(payload) } @@ -517,7 +518,7 @@ impl BusDevice for Ultra64 { // and lowers the GIO interrupt. Driver writes this before reading the packet. dlog!(LogModule::Ultra, "wr GIO_RDB_WRITE_INTR_REG (clear rdb_w_int, lower GioExp0)"); self.state.lock().rdb_w_int = false; - self.ioc.set_interrupt(IocInterrupt::GioExp0, false); + self.irq.set(false); } 0xC => { // GIO_RDB_READ_INTR_REG: W0C — driver writes 0 to clear product_id bit 30 @@ -529,7 +530,7 @@ impl BusDevice for Ultra64 { let new_ack = st.rdb_n2h_ack.wrapping_add(1); st.rdb_n2h_ack = new_ack; drop(st); - self.ioc.set_interrupt(IocInterrupt::GioExp0, false); + self.irq.set(false); self.hdr_mut().h2n_ring.push(h2n::RDB_ACK, new_ack); let _ = self.evt_h2n.set(EventState::Signaled); } diff --git a/src/dev/vino.rs b/src/dev/vino.rs index 82c027a9..240008ce 100644 --- a/src/dev/vino.rs +++ b/src/dev/vino.rs @@ -23,12 +23,7 @@ use crate::dev::saa7191::Saa7191; use crate::dev::cdmc::Cdmc; use crate::devlog::{LogModule, devlog_is_active}; use crate::video_source::{VideoSource, Field, FieldParity}; - -/// Interrupt callback — implemented by the machine glue to assert/deassert -/// the VINO interrupt line on the IOC. Keeps vino.rs free of IOC details. -pub trait VinoIrq: Send + Sync { - fn set_interrupt(&self, active: bool); -} +use crate::irq::IrqLine; // ─── GIO64 mapping ────────────────────────────────────────────────────────── @@ -357,7 +352,7 @@ impl DmaWake { #[derive(Clone)] pub struct Vino { state: Arc>, - irq: Arc>>>, + irq: Arc>>, sys_mem: Arc>>>, /// D0 / composite (SAA7191 path) — defaults to black field generator. source_d0: Arc>>>, @@ -385,7 +380,9 @@ impl Vino { } /// Set the interrupt callback (called from machine setup after IOC is ready). - pub fn set_irq(&self, irq: Arc) { + /// Wire the interrupt to `irq` (the IOC's video line on an Indy). Keeps + /// vino.rs free of IOC details. + pub fn set_irq(&self, irq: IrqLine) { *self.irq.lock() = Some(irq); } @@ -422,7 +419,7 @@ impl Vino { // ── Interrupt assertion ─────────────────────────────────────────────── - fn raise_interrupt(st: &mut VinoState, irq: &Option>, new_status: u32) { + fn raise_interrupt(st: &mut VinoState, irq: &Option, new_status: u32) { // Only bits enabled in CONTROL (shifted right by 1) appear in int_status. // ctrl bits [6:1] are the six enable bits; isr bits [5:0] are the six status bits. let enable_mask = (st.control >> 1) & isr::MASK; @@ -432,11 +429,11 @@ impl Vino { let newly_raised = !old & st.int_status; if newly_raised != 0 { if let Some(irq) = irq { - irq.set_interrupt(true); + irq.set(true); } } else if st.int_status == 0 && old != 0 { if let Some(irq) = irq { - irq.set_interrupt(false); + irq.set(false); } } } @@ -444,7 +441,7 @@ impl Vino { // ── Control register write ──────────────────────────────────────────── /// Returns true if a DMA channel was just enabled (caller should notify wake). - fn control_w(st: &mut VinoState, irq: &Option>, val: u32) -> bool { + fn control_w(st: &mut VinoState, irq: &Option, val: u32) -> bool { let old = st.control; st.control = val & ctrl::MASK; @@ -496,7 +493,7 @@ impl Vino { // ── Interrupt status write (write 0 to individual bits to clear) ────── - fn intr_status_w(st: &mut VinoState, irq: &Option>, val: u32) { + fn intr_status_w(st: &mut VinoState, irq: &Option, val: u32) { // A 0-bit in the written value clears the corresponding status bit. for bit in 0..6u32 { if val & (1 << bit) == 0 { diff --git a/src/dev/wd33c93a.rs b/src/dev/wd33c93a.rs index 2694f8f7..7cb4250c 100644 --- a/src/dev/wd33c93a.rs +++ b/src/dev/wd33c93a.rs @@ -1,4 +1,5 @@ use std::fs::OpenOptions; +use crate::irq::IrqLine; use std::cell::Cell; use std::collections::VecDeque; use std::thread; @@ -273,7 +274,9 @@ struct Wd33c93aState { // interrupt line low. This allows SELECT_ATN to push 0x11 then 0x8E as two // separate interrupts, matching real hardware behaviour. irq_fifo: VecDeque<(u8, Option, bool)>, // (status, phase, deferred) — deferred=true: worker drops lock before delivery - callback: Option>, + /// The chip's INT, as the board wires it. On the HPC3, dropping it also + /// settles the paired PDMA channel's half of the shared SCSI line. + irq: Option, // Debug tracking: last values returned for register reads last_read_asr: Option, last_read_reg: Option<(u8, u8)>, // (register, value) @@ -284,11 +287,6 @@ struct Wd33c93aState { deferred_int: Arc, } -pub trait ScsiCallback: Send + Sync { - /// Set the SCSI interrupt line. Calling with false also clears any paired - /// PDMA completion bit (implementors handle this internally). - fn set_interrupt(&self, level: bool); -} // Safety: `Cell` is never `Sync` regardless of `T` (interior mutability // without synchronization) — needed here because `cpu_cycles` (a `Cell`, @@ -323,17 +321,17 @@ pub struct Wd33c93a { } impl Wd33c93a { - pub fn new(dma: Option>, callback: Option>, heartbeat: Arc) -> Self { - Self::new_with_config(dma, callback, heartbeat, true) + pub fn new(dma: Option>, irq: Option, heartbeat: Arc) -> Self { + Self::new_with_config(dma, irq, heartbeat, true) } - pub fn new_with_config(dma: Option>, callback: Option>, heartbeat: Arc, deferred_int: bool) -> Self { - Self::new_with_id(dma, callback, heartbeat, deferred_int, 0) + pub fn new_with_config(dma: Option>, irq: Option, heartbeat: Arc, deferred_int: bool) -> Self { + Self::new_with_id(dma, irq, heartbeat, deferred_int, 0) } /// Same as `new_with_config`, but tags every log line with `id` (the /// HPC3 SCSI controller number) — see `Wd33c93aState.id`'s doc comment. - pub fn new_with_id(dma: Option>, callback: Option>, heartbeat: Arc, deferred_int: bool, id: u8) -> Self { + pub fn new_with_id(dma: Option>, irq: Option, heartbeat: Arc, deferred_int: bool, id: u8) -> Self { let deferred_int_arc = Arc::new(AtomicBool::new(deferred_int)); Self { state: Arc::new(Mutex::new(Wd33c93aState { @@ -356,7 +354,7 @@ impl Wd33c93a { xfer_direction_in: false, pending_cdb: Vec::new(), irq_fifo: VecDeque::new(), - callback, + irq, last_read_asr: None, last_read_reg: None, last_cmd: 0, @@ -1538,7 +1536,7 @@ impl Wd33c93aState { if new_int != old_int { wdt!("INT_LINE {} (asr={:02x})", if new_int != 0 { "ASSERT" } else { "DEASSERT" }, self.asr); - if let Some(cb) = &self.callback { cb.set_interrupt(new_int != 0); } + if let Some(irq) = &self.irq { irq.set(new_int != 0); } } } diff --git a/src/dev/z85c30.rs b/src/dev/z85c30.rs index 46ed7548..9e3da359 100644 --- a/src/dev/z85c30.rs +++ b/src/dev/z85c30.rs @@ -9,6 +9,7 @@ use std::os::unix::net::{UnixListener, UnixStream}; use std::path::Path; use std::net::{TcpListener, TcpStream}; use std::time::{Duration, Instant}; +use crate::irq::IrqLine; use std::thread; use std::io::Write as IoWrite; use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, Device, Resettable, Saveable}; @@ -115,10 +116,6 @@ pub mod wr14 { pub const AUTO_ECHO: u8 = 1 << 4; } -pub trait IrqCallback: Send + Sync { - fn set_level(&self, level: bool); -} - pub struct Channel { pub regs: [u8; 16], pub reg_ptr: u8, @@ -129,14 +126,14 @@ pub struct Channel { pub tx_delay: u64, pub ip_num: Arc, pub ip_other: Arc, - pub callback: Option>, + pub irq: Option, // Z85C30 Tx interrupt fires once when buffer empties, then arms again only // after RES_Tx_P. Without this latch the IRQ line stays asserted forever. pub tx_int_pending: bool, } impl Channel { - pub fn new(name: &str, ip_num: Arc, ip_other: Arc, callback: Option>) -> Self { + pub fn new(name: &str, ip_num: Arc, ip_other: Arc, irq: Option) -> Self { let mut c = Self { regs: [0; 16], reg_ptr: 0, @@ -147,7 +144,7 @@ impl Channel { tx_delay: 0, ip_num, ip_other, - callback, + irq, tx_int_pending: false, }; c.update_tx_delay(); @@ -300,9 +297,9 @@ impl Channel { let ip = self.get_ip(); self.ip_num.store(ip, Ordering::SeqCst); - if let Some(cb) = &self.callback { + if let Some(line) = &self.irq { let other = self.ip_other.load(Ordering::SeqCst); - cb.set_level((ip | other) != 0); + line.set((ip | other) != 0); } } @@ -609,24 +606,24 @@ pub const DEFAULT_PORTS: [u16; 2] = [8880, 8881]; impl Z85c30 { /// Default constructor: binds TCP serial backends on 127.0.0.1:8880 /// (channel A / tty2) and 127.0.0.1:8881 (channel B / tty1). - pub fn new(callback: Option>) -> Self { - Self::new_with_ports(callback, DEFAULT_PORTS) + pub fn new(irq: Option) -> Self { + Self::new_with_ports(irq, DEFAULT_PORTS) } /// As `new`, with the TCP ports of channels A and B on 127.0.0.1. - pub fn new_with_ports(callback: Option>, ports: [u16; 2]) -> Self { - Self::new_inner(callback, Some(ports)) + pub fn new_with_ports(irq: Option, ports: [u16; 2]) -> Self { + Self::new_inner(irq, Some(ports)) } /// CI-mode constructor: uses null backends instead of binding TCP. The /// caller is expected to install real backends via `set_backend_a` / /// `set_backend_b` before the first `start()`. Avoids port conflicts /// when multiple `--ci` instances run in parallel. - pub fn new_null(callback: Option>) -> Self { - Self::new_inner(callback, None) + pub fn new_null(irq: Option) -> Self { + Self::new_inner(irq, None) } - fn new_inner(callback: Option>, ports: Option<[u16; 2]>) -> Self { + fn new_inner(irq: Option, ports: Option<[u16; 2]>) -> Self { let ip_a = Arc::new(AtomicU8::new(0)); let ip_b = Arc::new(AtomicU8::new(0)); @@ -649,9 +646,9 @@ impl Z85c30 { }; Self { - channel_a: Arc::new((Mutex::new(Channel::new("A", ip_a.clone(), ip_b.clone(), callback.clone())), Condvar::new())), + channel_a: Arc::new((Mutex::new(Channel::new("A", ip_a.clone(), ip_b.clone(), irq.clone())), Condvar::new())), // Note: Channel B gets ip_b as its 'num' and ip_a as 'other' - channel_b: Arc::new((Mutex::new(Channel::new("B", ip_b, ip_a, callback)), Condvar::new())), + channel_b: Arc::new((Mutex::new(Channel::new("B", ip_b, ip_a, irq)), Condvar::new())), backend_a: Arc::new(Mutex::new(backend_a)), backend_b: Arc::new(Mutex::new(backend_b)), inject_b: Arc::new(Mutex::new(VecDeque::new())), @@ -1437,11 +1434,6 @@ mod tests { #[test] fn restore_redrives_irq_line() { - struct Line(AtomicBool); - impl IrqCallback for Line { - fn set_level(&self, level: bool) { self.0.store(level, Ordering::SeqCst); } - } - let src = Z85c30::new_null(None); { let mut ch = src.channel_a.0.lock(); @@ -1450,11 +1442,11 @@ mod tests { } let saved = src.save_state(); - let line = Arc::new(Line(AtomicBool::new(false))); - let dst = Z85c30::new_null(Some(line.clone())); + let (probe, line) = crate::irq::LevelProbe::line(); + let dst = Z85c30::new_null(Some(line)); dst.load_state(&saved).expect("load_state"); - assert!(line.0.load(Ordering::SeqCst), + assert!(probe.level(), "`power_on_devices` cleared map_stat SERIAL and `Ioc::load_state` \ restored it wholesale, so only the SCC can put the line back up"); } diff --git a/src/irq.rs b/src/irq.rs new file mode 100644 index 00000000..6760ddbd --- /dev/null +++ b/src/irq.rs @@ -0,0 +1,98 @@ +//! Interrupt lines between devices and the controllers they report to. +//! +//! A device holds an [`IrqLine`]: which controller, and which of its inputs. +//! It drives the line to its level when its state may have changed, and the +//! controller decides what that means: a status bit behind a mask, an input +//! ORed with others, or a cascade into a controller further up (the IOC's +//! local and mappable sources into the CPU's interrupt pins). Devices no +//! longer need a callback trait of their own for each chip that collects +//! their interrupt. + +use std::sync::Arc; + +/// Something interrupt lines lead into. +pub trait InterruptController: Send + Sync { + /// Drive input `line` to `level`. An input the controller latches takes + /// a rising level as its edge. + fn set_line(&self, line: u32, level: bool); +} + +/// One input of one controller, as a device sees it. +#[derive(Clone)] +pub struct IrqLine { + ctrl: Arc, + line: u32, +} + +impl IrqLine { + pub fn new(ctrl: Arc, line: u32) -> Self { + Self { ctrl, line } + } + + /// A line that leads nowhere, for a device built on its own (tests). + pub fn unconnected() -> Self { + Self::new(Arc::new(Unconnected), 0) + } + + pub fn set(&self, level: bool) { + self.ctrl.set_line(self.line, level); + } + + pub fn line(&self) -> u32 { + self.line + } +} + +struct Unconnected; + +/// A controller with one input, for tests: the level last driven. +#[cfg(test)] +#[derive(Default)] +pub struct LevelProbe(std::sync::Mutex); + +#[cfg(test)] +impl LevelProbe { + /// A probe and a line into it. + pub fn line() -> (Arc, IrqLine) { + let p = Arc::new(Self::default()); + (p.clone(), IrqLine::new(p, 0)) + } + + pub fn level(&self) -> bool { + *self.0.lock().unwrap() + } +} + +#[cfg(test)] +impl InterruptController for LevelProbe { + fn set_line(&self, _line: u32, level: bool) { + *self.0.lock().unwrap() = level; + } +} + +impl InterruptController for Unconnected { + fn set_line(&self, _line: u32, _level: bool) {} +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + struct Recorder(Mutex>); + impl InterruptController for Recorder { + fn set_line(&self, line: u32, level: bool) { + self.0.lock().unwrap().push((line, level)); + } + } + + #[test] + fn a_line_drives_its_controllers_input() { + let r = Arc::new(Recorder(Mutex::new(Vec::new()))); + let l = IrqLine::new(r.clone(), 5); + l.set(true); + l.clone().set(false); + assert_eq!(*r.0.lock().unwrap(), vec![(5, true), (5, false)]); + IrqLine::unconnected().set(true); + } +} diff --git a/src/lib.rs b/src/lib.rs index 687b7031..7cbbbffd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,6 +181,7 @@ pub mod bench_runner; pub mod cow_disk; pub mod chd_disk; pub mod scsi; +pub mod irq; pub mod ui; pub mod cpu; pub mod dev; diff --git a/src/machine.rs b/src/machine.rs index f45b75fd..cd485b2a 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -553,15 +553,8 @@ impl Machine { } else { let r = Arc::new(Rex3::new(heartbeat.clone(), fasttick_count.clone(), decoded_count.clone(), Arc::clone(&l1i_hit_count), Arc::clone(&l1i_fetch_count), Arc::clone(&uncached_fetch_count))); let wiring = GIO_SLOT_MAP[profile_idx(guinness)][GioSlot::Gfx as usize]; - let ioc_clone = ioc.clone(); - r.set_vblank_callback(Arc::new(move |active| { - if active { crate::pf_instant!(crate::perfetto::events::REX3_VBLANK); } - ioc_clone.set_interrupt(wiring.retrace, active); - })); - let ioc_ff = ioc.clone(); - r.set_fifo_full_callback(Arc::new(move |active| { - ioc_ff.set_interrupt(wiring.fifo_full, active); - })); + r.set_vblank_irq(ioc.irq_line(wiring.retrace)); + r.set_fifo_full_irq(ioc.irq_line(wiring.fifo_full)); Some(r) }; @@ -570,15 +563,8 @@ impl Machine { } else { let r = Arc::new(Rex3::new(heartbeat.clone(), fasttick_count.clone(), decoded_count.clone(), Arc::clone(&l1i_hit_count), Arc::clone(&l1i_fetch_count), Arc::clone(&uncached_fetch_count))); let wiring = GIO_SLOT_MAP[profile_idx(guinness)][GioSlot::Exp0 as usize]; - let ioc_clone = ioc.clone(); - r.set_vblank_callback(Arc::new(move |active| { - if active { crate::pf_instant!(crate::perfetto::events::REX3_VBLANK); } - ioc_clone.set_interrupt(wiring.retrace, active); - })); - let ioc_ff = ioc.clone(); - r.set_fifo_full_callback(Arc::new(move |active| { - ioc_ff.set_interrupt(wiring.fifo_full, active); - })); + r.set_vblank_irq(ioc.irq_line(wiring.retrace)); + r.set_fifo_full_irq(ioc.irq_line(wiring.fifo_full)); Some(r) }; @@ -614,14 +600,17 @@ impl Machine { crate::config::GraphicsBoard::SolidImpact | crate::config::GraphicsBoard::HighImpact | crate::config::GraphicsBoard::MaxImpact => { - Some(crate::dev::mgras::Mgras::new(cfg.graphics.board, ioc.clone(), heartbeat.clone(), fasttick_count.clone())) + Some(crate::dev::mgras::Mgras::new(cfg.graphics.board, { + use crate::dev::ioc::IocInterrupt::{GioSgFifo, GioSgGraphics, GioSgRetrace}; + [ioc.irq_line(GioSgFifo), ioc.irq_line(GioSgGraphics), ioc.irq_line(GioSgRetrace)] + }, heartbeat.clone(), fasttick_count.clone())) } _ => None, }; // N64 development board (Ultra64) — GIO slot 0 at 0x1F400000 let ultra64: Option> = if cfg.ultra64.enabled { - match crate::dev::ultra64::Ultra64::new(ioc.clone()) { + match crate::dev::ultra64::Ultra64::new(ioc.irq_line(crate::dev::ioc::IocInterrupt::GioExp0)) { Ok(dev) => Some(Arc::new(dev)), Err(e) => { eprintln!("ultra64: failed to initialize ({e}); running without N64 dev board"); @@ -634,15 +623,7 @@ impl Machine { // VINO (Video-In, No Out) — GIO64 at 0x1F080000 let vino = crate::dev::vino::Vino::new(); - { - struct VinoIrqAdapter { ioc: crate::dev::ioc::Ioc } - impl crate::dev::vino::VinoIrq for VinoIrqAdapter { - fn set_interrupt(&self, active: bool) { - self.ioc.set_interrupt(crate::dev::ioc::IocInterrupt::VideoVsync, active); - } - } - vino.set_irq(Arc::new(VinoIrqAdapter { ioc: ioc.clone() })); - } + vino.set_irq(ioc.irq_line(crate::dev::ioc::IocInterrupt::VideoVsync)); // 2. Create Physical Bus with devices // `cfg` is shadowed by MipsCpuConfig further down; grab this first. @@ -695,7 +676,7 @@ impl Machine { // Connect Physical to MC (for VDMA) mc.set_phys(phys.clone()); - mc.set_ioc(ioc.clone()); + mc.set_dma_irq(ioc.irq_line(crate::dev::ioc::IocInterrupt::McDma)); // Wire MEMCFG callback: when MC writes MEMCFG0/1, remap banks in Physical. // SAFETY: Physical is pinned in Arc; remap_banks(&mut self) is only invoked diff --git a/src/platform_profile_tests.rs b/src/platform_profile_tests.rs index 7a852196..8a7803d5 100644 --- a/src/platform_profile_tests.rs +++ b/src/platform_profile_tests.rs @@ -217,8 +217,7 @@ mod tests { #[test] fn mgras_answers_the_gio_id_probe() { let hb = Arc::new(std::sync::atomic::AtomicU64::new(0)); - let ioc = crate::dev::ioc::Ioc::new(false); - let m = Mgras::new(GraphicsBoard::SolidImpact, ioc, hb.clone(), hb); + let m = Mgras::new(GraphicsBoard::SolidImpact, std::array::from_fn(|_| crate::irq::IrqLine::unconnected()), hb.clone(), hb); assert_eq!(m.read32(MGRAS_SLOT_GFX_BASE).data, GIO_ID); }