From c1be966d8fe1002c7e1a51b5074ffb84941a259b Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:39:38 -0800 Subject: [PATCH 01/10] irq: an interrupt-controller trait and the lines devices hold into one `InterruptController::set_line(line, level)` is what interrupt lines lead into; an `IrqLine` is one input of one controller, which a device drives to its level. One shape for every device's interrupt, instead of a callback trait per device. Co-Authored-By: Claude Opus 5.5 --- src/irq.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 99 insertions(+) create mode 100644 src/irq.rs 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; From 18e2accb884306060012bdbc302400a8d2128ad4 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:39:38 -0800 Subject: [PATCH 02/10] ioc: the IOC is an interrupt controller Each IocInterrupt is a line, numbered in declaration order. Ioc and the lines from `Ioc::irq_line` drive the same code `set_interrupt` always ran, now `IocState::set_source`. Nothing uses the lines yet; the devices move over one at a time. Co-Authored-By: Claude Opus 5.5 --- src/dev/ioc.rs | 221 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 72 deletions(-) diff --git a/src/dev/ioc.rs b/src/dev/ioc.rs index 7cfc7705..e0040dc9 100644 --- a/src/dev/ioc.rs +++ b/src/dev/ioc.rs @@ -10,6 +10,7 @@ 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::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 @@ -431,6 +455,18 @@ struct IocIrqLine { 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 { state: Arc>, source: IocInterrupt, @@ -475,6 +511,15 @@ impl Ps2Callback for IocIrqLine { } } +/// 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); + } + } +} + #[derive(Clone)] pub struct Ioc { state: Arc>, @@ -651,79 +696,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 +1180,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 +1398,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. From 12c3b7cb68cdffe0cb6685ff99629e8bc8f7fae2 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:42:23 -0800 Subject: [PATCH 03/10] z85c30: the SCC drives an IrqLine Its interrupt goes to the IOC's Serial line instead of through its own IrqCallback trait, which goes. The level is the same: either channel's pending bits. Co-Authored-By: Claude Opus 5.5 --- src/dev/ioc.rs | 23 +++++------------------ src/dev/z85c30.rs | 44 ++++++++++++++++++-------------------------- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/dev/ioc.rs b/src/dev/ioc.rs index e0040dc9..72995dcd 100644 --- a/src/dev/ioc.rs +++ b/src/dev/ioc.rs @@ -5,7 +5,7 @@ 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}; @@ -493,21 +493,11 @@ 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 - } - state.update_interrupts(); - } -} - impl Ps2Callback for IocIrqLine { fn set_interrupt(&self, active: bool) { - self.set_level(active); + let mut state = self.state.lock(); + if active { state.map_stat |= map_regs::KBD_MOUSE } else { state.map_stat &= !map_regs::KBD_MOUSE } + state.update_interrupts(); } } @@ -600,10 +590,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 { 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"); } From 3b5f6129701a9d6f3f8c174813432075e2f359bc Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:47:00 -0800 Subject: [PATCH 04/10] ps2: the 8042 drives an IrqLine Its interrupt goes to the IOC's KbMouse line instead of through its own Ps2Callback trait; that trait and the IOC's IocIrqLine adapter go. Co-Authored-By: Claude Opus 5.5 --- src/dev/ioc.rs | 22 +++------------------- src/dev/ps2.rs | 30 +++++++++++++++++++----------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/dev/ioc.rs b/src/dev/ioc.rs index 72995dcd..abaddffa 100644 --- a/src/dev/ioc.rs +++ b/src/dev/ioc.rs @@ -8,7 +8,7 @@ use crate::snapshot::{get_field, toml_u8, hex_u8}; 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; @@ -450,11 +450,6 @@ 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>); @@ -493,14 +488,6 @@ impl TimerCallback for IocTimerCallback { } } -impl Ps2Callback for IocIrqLine { - fn set_interrupt(&self, active: bool) { - let mut state = self.state.lock(); - if active { state.map_stat |= map_regs::KBD_MOUSE } else { state.map_stat &= !map_regs::KBD_MOUSE } - state.update_interrupts(); - } -} - /// Line `n` is the [`IocInterrupt`] numbered `n`, as through [`Ioc::irq_line`]. impl InterruptController for Ioc { fn set_line(&self, line: u32, level: bool) { @@ -605,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)) @@ -620,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()), 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); From 286c1f40f7361cd1fb8e0317cab308267e99b5b4 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:50:03 -0800 Subject: [PATCH 05/10] vino: VINO drives an IrqLine The IOC's VideoVsync line, from `Ioc::irq_line`, replaces the VinoIrq trait and the machine's adapter for it. Co-Authored-By: Claude Opus 5.5 --- src/dev/vino.rs | 23 ++++++++++------------- src/machine.rs | 10 +--------- 2 files changed, 11 insertions(+), 22 deletions(-) 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/machine.rs b/src/machine.rs index f45b75fd..b0940fb9 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -634,15 +634,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. From 4d51dfe11871c686029c4191964fc198ce137932 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:57:00 -0800 Subject: [PATCH 06/10] hpc3: INTSTAT is an interrupt controller; SCSI, PDMA and SEEQ drive lines Each INTSTAT bit is a line. A SCSI channel's chip and DMA bits are ORed onto its IOC line, and a chip-INT ack still settles the paired PDMA half; the SEEQ's level goes to the IOC's Ethernet line as before, not ORed with the Ethernet DMA bits. The WD33C93A, SEEQ 8003 and PDMA channels hold IrqLines; ScsiCallback, SeeqCallback, PdmaCallback and the Hpc3Irq/EnetSeeqIrq adapters go. Same bits, same lock order. Co-Authored-By: Claude Opus 5.5 --- src/dev/hpc3.rs | 268 ++++++++++++++++++++++++-------------------- src/dev/seeq8003.rs | 41 ++++--- src/dev/wd33c93a.rs | 24 ++-- 3 files changed, 179 insertions(+), 154 deletions(-) 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/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/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); } } } From 9ae30312ec8ea2c2873e264da7c08549586f17c8 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:04:12 -0800 Subject: [PATCH 07/10] mc: the GIO DMA engine drives an IrqLine The MC held the whole IOC to raise and clear one interrupt; it now holds the IOC's McDma line (`set_dma_irq`). Co-Authored-By: Claude Opus 5.5 --- src/dev/mc.rs | 18 ++++++++++-------- src/dev/mc_vdma.rs | 5 ++--- src/machine.rs | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) 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/machine.rs b/src/machine.rs index b0940fb9..0bf01de1 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -687,7 +687,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 From b3b07830e9a0bada4090603ac8a192433df9107b Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:04:41 -0800 Subject: [PATCH 08/10] ultra64: the board drives an IrqLine It held the whole IOC for one line; it now holds the IOC's GioExp0 line. Its three sources still share that line with no OR between them, as before (an ack of one drops the line while another may be pending); that is a separate fix. Co-Authored-By: Claude Opus 5.5 --- src/dev/ultra64.rs | 23 ++++++++++++----------- src/machine.rs | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) 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/machine.rs b/src/machine.rs index 0bf01de1..4705bec2 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -621,7 +621,7 @@ impl Machine { // 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"); From d584117364c7ab54281daea4a7b00c716bd5295e Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:07:46 -0800 Subject: [PATCH 09/10] mgras: the board drives three IrqLines The IMPACT board picked its IOC sources itself (the Indigo2 graphics slot's FIFO, general and retrace); the machine now hands it those three lines, so the board no longer knows which slot it is in. Co-Authored-By: Claude Opus 5.5 --- src/dev/mgras/mod.rs | 23 +++++++++++++---------- src/dev/mgras/record.rs | 2 +- src/machine.rs | 5 ++++- src/platform_profile_tests.rs | 3 +-- 4 files changed, 19 insertions(+), 14 deletions(-) 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/machine.rs b/src/machine.rs index 4705bec2..85181f5e 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -614,7 +614,10 @@ 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, }; 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); } From 56dffc2b12531703aff6b0627f2478799eb8d4fc Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:11:02 -0800 Subject: [PATCH 10/10] rex3: Newport's VV_INT_N and FIFO_INT_N are IrqLines The machine hands REX3 the IOC lines its slot is wired to (`set_vblank_irq`, `set_fifo_full_irq`) instead of closures that called the IOC; the vblank trace instant moves to where REX3 raises the line. Co-Authored-By: Claude Opus 5.5 --- src/dev/ng1/rex3.rs | 40 ++++++++++++++++++++++++---------------- src/machine.rs | 22 ++++------------------ 2 files changed, 28 insertions(+), 34 deletions(-) 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/machine.rs b/src/machine.rs index 85181f5e..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) };