From 67399503f6b3a5d7c8b063f4c652d1bbbe4c5776 Mon Sep 17 00:00:00 2001 From: Bernie Innocenti Date: Wed, 8 Jul 2026 01:27:06 +0900 Subject: [PATCH 1/2] config: unify A2091/A4091 into [scsi] controller Fold the separate [a4091] section into [scsi] with a `controller` key ("a2091" default, or "a4091"). ScsiConfig gains the ScsiController enum and emulator build switches on it, instead of two independent boards. The two controllers were mutually exclusive in practice, and a single section lets the launcher round-trip the config without dropping a board. rom_odd is validated as an A2091 split-EPROM option. [a4091] is removed outright (the feature is new enough to have no install base). Co-Authored-By: Claude Opus 4.8 --- copperline.example.toml | 21 ++++--- src/config.rs | 131 +++++++++++++++++----------------------- src/emulator.rs | 89 +++++++++++++++------------ 3 files changed, 117 insertions(+), 124 deletions(-) diff --git a/copperline.example.toml b/copperline.example.toml index 1e549aa..2ba69d1 100644 --- a/copperline.example.toml +++ b/copperline.example.toml @@ -172,15 +172,20 @@ slow = "512K" # metadata = "boards/megaram.toml" -# A2091 SCSI controller: a Zorro II board with up to seven drives, on any -# machine model. Preferred over [ide] for multiple disks: the board's own -# boot ROM carries scsi.device and autoboots on Kickstart 1.3+, so it does -# not depend on the Kickstart IDE driver (which only probes the master on -# stock 3.1). Needs an A590/A2091 boot ROM image (6.6+, 16K/32K; rom_odd -# takes the odd half of split even/odd EPROM dumps). Drive paths accept -# the same images as [ide]: RDB HDFs, bare partition hardfiles, or host -# directories. +# SCSI controller: a Zorro board with up to seven drives, on any machine +# model. Preferred over [ide] for multiple disks: the board's own boot ROM +# carries scsi.device and autoboots on Kickstart 1.3+, so it does not depend +# on the Kickstart IDE driver (which only probes the master on stock 3.1). +# Drive paths accept the same images as [ide]: RDB HDFs, bare partition +# hardfiles, or host directories. +# +# controller picks the board: "a2091" (Zorro II, WD33C93; the default) or +# "a4091" (Zorro III, NCR 53C710). The A2091 needs an A590/A2091 boot ROM +# (6.6+, 16K/32K; rom_odd takes the odd half of a split even/odd EPROM dump); +# the A4091 needs a raw A4091 EPROM image (e.g. the open-source a4091.rom) and +# has a single ROM (no rom_odd). # [scsi] +# controller = "a2091" # rom = "a2091-v6.6.rom" # unit0 = "workbench.hdf" # unit1 = "data.hdf" diff --git a/src/config.rs b/src/config.rs index e5a6204..e2e16b3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -112,14 +112,11 @@ pub struct Config { /// Gayle IDE drive images (raw flat HDF, RDB inside), opened /// read/write. Only valid on machines with a Gayle gate array. pub ide: IdeConfig, - /// A2091 SCSI controller (`[scsi]`): boot ROM image plus up to seven - /// drive images on SCSI IDs 0-6. Works on any machine model (the board - /// autoconfigs on the Zorro chain and carries its own scsi.device). + /// SCSI controller (`[scsi]`): the `controller` selects an A2091 (Zorro II) + /// or A4091 (Zorro III), plus a boot ROM image and up to seven drive images + /// on SCSI IDs 0-6. The board autoconfigs on the Zorro chain and carries its + /// own scsi.device. pub scsi: ScsiConfig, - /// A4091 SCSI-2 controller (`[a4091]`): boot ROM image plus up to seven - /// drive images on SCSI IDs 0-6. A Zorro III board; the 53C710 core is - /// not implemented yet, so drives are parsed but not driven. - pub a4091: A4091Config, /// A2065 Ethernet board (`[a2065]`): when set, an A2065 NIC autoconfigs on /// the Zorro chain using the named host network backend. Networking is /// non-deterministic, so a fitted A2065 breaks byte-identical replay. @@ -294,34 +291,34 @@ pub struct IdeConfig { pub slave: Option, } +/// Which SCSI host adapter the `[scsi]` section fits. Both are Zorro +/// autoconfig boards carrying their own boot ROM and scsi.device. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum ScsiController { + /// Commodore A2091/A590: Zorro II, WD33C93. The default. + #[default] + A2091, + /// Commodore A4091: Zorro III, NCR 53C710. + A4091, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ScsiConfig { - /// A2091/A590 boot ROM image (merged, or the even half when - /// `rom_odd` gives the other EPROM). + /// Which host adapter the section fits (`controller`). Only meaningful + /// when `enabled()`. + pub controller: ScsiController, + /// Boot ROM image. For the A2091's split even/odd EPROM dumps, `rom` is + /// the even half and `rom_odd` the other; the A4091 has a single ROM. pub rom: Option, - /// Odd-byte EPROM half for split dumps. + /// Odd-byte EPROM half for split A2091 dumps. pub rom_odd: Option, /// Drive images by SCSI ID (0-6; ID 7 is the controller). pub units: [Option; 7], } impl ScsiConfig { - /// Whether a `[scsi]` section asked for the board at all. - pub fn enabled(&self) -> bool { - self.rom.is_some() || self.units.iter().any(Option::is_some) - } -} - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct A4091Config { - /// A4091 boot ROM image (a raw 32K or 64K byte-wide EPROM dump). - pub rom: Option, - /// Drive images by SCSI ID (0-6; ID 7 is the controller). - pub units: [Option; 7], -} - -impl A4091Config { - /// Whether an `[a4091]` section asked for the board at all. + /// Whether a `[scsi]` section asked for a board at all (a bare + /// `controller` with no ROM or drives fits nothing). pub fn enabled(&self) -> bool { self.rom.is_some() || self.units.iter().any(Option::is_some) } @@ -899,7 +896,6 @@ impl Default for Config { audio: AudioConfig::default(), ide: IdeConfig::default(), scsi: ScsiConfig::default(), - a4091: A4091Config::default(), a2065_net: None, floppy: FloppyConfig::default(), floppy_connected: [true, false, false, false], @@ -1165,8 +1161,6 @@ pub struct RawConfig { #[serde(default, skip_serializing_if = "is_default")] pub(crate) scsi: RawScsi, #[serde(default, skip_serializing_if = "is_default")] - pub(crate) a4091: RawA4091, - #[serde(default, skip_serializing_if = "is_default")] pub(crate) a2065: RawA2065, #[serde(default, skip_serializing_if = "is_default")] pub(crate) floppy: RawFloppy, @@ -1347,8 +1341,11 @@ pub(crate) struct RawIde { #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct RawScsi { - /// A2091/A590 boot ROM image. For split even/odd EPROM dumps, `rom` - /// is the even half and `rom_odd` the odd half. + /// Host adapter to fit: "a2091" (Zorro II, default) or "a4091" (Zorro III). + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) controller: Option, + /// Boot ROM image. For split even/odd A2091 EPROM dumps, `rom` is the + /// even half and `rom_odd` the odd half. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) rom: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -1369,29 +1366,6 @@ pub(crate) struct RawScsi { pub(crate) unit6: Option, } -/// `[a4091]` SCSI-2 controller: boot ROM image plus drive images by SCSI ID. -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub(crate) struct RawA4091 { - /// A4091 boot ROM image (raw 32K or 64K byte-wide EPROM dump). - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) rom: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit0: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit1: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit2: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit3: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit4: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit5: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) unit6: Option, -} - /// `[a2065]` Ethernet board. Fitting the board enables host networking, which /// is non-deterministic. #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -1844,7 +1818,22 @@ impl TryFrom for Config { )); } + let scsi_controller = match raw.scsi.controller.as_deref() { + None => ScsiController::A2091, + Some(raw_ctrl) => match raw_ctrl.trim().to_ascii_lowercase().as_str() { + "a2091" => ScsiController::A2091, + "a4091" => ScsiController::A4091, + _ => { + errors.push(anyhow!( + "[scsi] controller = {raw_ctrl:?} is not known \ + (expected \"a2091\" or \"a4091\")" + )); + ScsiController::A2091 + } + }, + }; let scsi = ScsiConfig { + controller: scsi_controller, rom: raw.scsi.rom.map(PathBuf::from), rom_odd: raw.scsi.rom_odd.map(PathBuf::from), units: [ @@ -1858,33 +1847,24 @@ impl TryFrom for Config { ], }; if scsi.enabled() && scsi.rom.is_none() { + let hint = match scsi.controller { + ScsiController::A2091 => { + "an A590/A2091 6.x ROM image; its scsi.device drives the disks" + } + ScsiController::A4091 => "a raw A4091 EPROM image, e.g. the open-source a4091.rom", + }; errors.push(anyhow!( - "[scsi] drives need the A2091 boot ROM: set [scsi] rom = \"...\" \ - (an A590/A2091 6.x ROM image; its scsi.device drives the disks)" + "[scsi] drives need the boot ROM: set [scsi] rom = \"...\" ({hint})" )); } - if scsi.rom_odd.is_some() && scsi.rom.is_none() { - errors.push(anyhow!("[scsi] rom_odd needs rom (the even EPROM half)")); - } - - let a4091 = A4091Config { - rom: raw.a4091.rom.map(PathBuf::from), - units: [ - raw.a4091.unit0.map(drive_image).transpose()?, - raw.a4091.unit1.map(drive_image).transpose()?, - raw.a4091.unit2.map(drive_image).transpose()?, - raw.a4091.unit3.map(drive_image).transpose()?, - raw.a4091.unit4.map(drive_image).transpose()?, - raw.a4091.unit5.map(drive_image).transpose()?, - raw.a4091.unit6.map(drive_image).transpose()?, - ], - }; - if a4091.enabled() && a4091.rom.is_none() { + if scsi.rom_odd.is_some() && scsi.controller != ScsiController::A2091 { errors.push(anyhow!( - "[a4091] drives need the boot ROM: set [a4091] rom = \"...\" \ - (a raw A4091 EPROM image, e.g. the open-source a4091.rom)" + "[scsi] rom_odd is an A2091 split-EPROM option; the A4091 has a single rom" )); } + if scsi.rom_odd.is_some() && scsi.rom.is_none() { + errors.push(anyhow!("[scsi] rom_odd needs rom (the even EPROM half)")); + } let a2065_net = match &raw.a2065.net { None => None, @@ -2009,7 +1989,6 @@ impl TryFrom for Config { audio, ide, scsi, - a4091, a2065_net, floppy, floppy_connected, diff --git a/src/emulator.rs b/src/emulator.rs index dabc05c..b7e4473 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1780,48 +1780,57 @@ pub fn build_machine( // is attached to the bus after it is built; the slot index ties them. let mut devices: Vec = Vec::new(); if cfg.scsi.enabled() { + use crate::config::ScsiController; let rom_path = cfg.scsi.rom.as_ref().expect("config validated [scsi] rom"); - let rom = crate::a2091::A2091::load_rom(rom_path, cfg.scsi.rom_odd.as_deref())?; - let mut board = crate::a2091::A2091::new(rom)?; - for (unit, drive) in cfg.scsi.units.iter().enumerate() { - let Some(drive) = drive else { continue }; - board.attach_drive( - unit, - crate::scsi::ScsiDisk::open(&drive.path, unit, drive.volume_name.as_deref())?, - ); - info!("scsi: unit {unit} {}", drive.path.display()); - } - let slot = devices.len(); - zorro.add_board(crate::zorro::BoardSpec::a2091(slot))?; - info!( - "scsi: A2091 controller on the Zorro chain (slot {slot}), ROM {}", - rom_path.display() - ); - devices.push(crate::zorro_device::BoardDevice::A2091(board)); - } - if cfg.a4091.enabled() { - let rom_path = cfg - .a4091 - .rom - .as_ref() - .expect("config validated [a4091] rom"); - let rom = crate::a4091::A4091::load_rom(rom_path)?; - let mut board = crate::a4091::A4091::new(rom)?; - for (unit, drive) in cfg.a4091.units.iter().enumerate() { - let Some(drive) = drive else { continue }; - board.attach_drive( - unit, - crate::scsi::ScsiDisk::open(&drive.path, unit, drive.volume_name.as_deref())?, - ); - info!("a4091: unit {unit} {}", drive.path.display()); - } let slot = devices.len(); - zorro.add_board(crate::zorro::BoardSpec::a4091(slot))?; - info!( - "a4091: SCSI controller on the Zorro chain (slot {slot}), ROM {}", - rom_path.display() - ); - devices.push(crate::zorro_device::BoardDevice::A4091(board)); + // The controller picks the board; the drive plumbing is identical. + let device = match cfg.scsi.controller { + ScsiController::A2091 => { + let rom = crate::a2091::A2091::load_rom(rom_path, cfg.scsi.rom_odd.as_deref())?; + let mut board = crate::a2091::A2091::new(rom)?; + for (unit, drive) in cfg.scsi.units.iter().enumerate() { + let Some(drive) = drive else { continue }; + board.attach_drive( + unit, + crate::scsi::ScsiDisk::open( + &drive.path, + unit, + drive.volume_name.as_deref(), + )?, + ); + info!("scsi: unit {unit} {}", drive.path.display()); + } + zorro.add_board(crate::zorro::BoardSpec::a2091(slot))?; + info!( + "scsi: A2091 controller on the Zorro chain (slot {slot}), ROM {}", + rom_path.display() + ); + crate::zorro_device::BoardDevice::A2091(board) + } + ScsiController::A4091 => { + let rom = crate::a4091::A4091::load_rom(rom_path)?; + let mut board = crate::a4091::A4091::new(rom)?; + for (unit, drive) in cfg.scsi.units.iter().enumerate() { + let Some(drive) = drive else { continue }; + board.attach_drive( + unit, + crate::scsi::ScsiDisk::open( + &drive.path, + unit, + drive.volume_name.as_deref(), + )?, + ); + info!("scsi: unit {unit} {}", drive.path.display()); + } + zorro.add_board(crate::zorro::BoardSpec::a4091(slot))?; + info!( + "scsi: A4091 controller on the Zorro chain (slot {slot}), ROM {}", + rom_path.display() + ); + crate::zorro_device::BoardDevice::A4091(board) + } + }; + devices.push(device); } // WASM plugin boards: assign each a device slot, put its autoconfig // identity on the chain, and instantiate the module. From d79bbfa635f37899446de391ef0e53d201dfcfd3 Mon Sep 17 00:00:00 2001 From: Bernie Innocenti Date: Wed, 8 Jul 2026 01:27:06 +0900 Subject: [PATCH 2/2] launcher: add SCSI controller picker Add a "SCSI controller" cycle (None / A2091 (Z2) / A4091 (Z3)) to the Hard Disk tab, driving the shared boot-ROM and unit rows. The ROM and drives grey out with no controller; rom_odd greys unless A2091. Keeps the tab compact instead of listing a second seven-drive controller. Co-Authored-By: Claude Opus 4.8 --- src/video/launcher.rs | 111 ++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/src/video/launcher.rs b/src/video/launcher.rs index e95ad12..4aa48b3 100644 --- a/src/video/launcher.rs +++ b/src/video/launcher.rs @@ -24,7 +24,7 @@ use crate::chipset::denise::DeniseRevision; use crate::config::{ format_size, machine_profile_defaults, ChannelMode, Chipset, Config, CpuModel, JoystickInputMode, MachineModel, Overscan, PacingBudget, PixelAspect, RawConfig, RawDrive, - RawFloppyDrive, RawZorroBoard, SerialMode, WarpSpeed, + RawFloppyDrive, RawZorroBoard, ScsiController, SerialMode, WarpSpeed, }; use crate::zorro::{ConfigOption, ConfigOptionKind, LoadedZorroBoard}; use anyhow::Result; @@ -248,6 +248,7 @@ pub enum LauncherField { // Hard disk IdeMaster, IdeSlave, + ScsiController, ScsiRom, ScsiRomOdd, ScsiUnit0, @@ -352,9 +353,10 @@ const FLOPPY_ROWS: [Row; 9] = [ row(F::Df3Image, "DF3 image", PathRow), row(F::Df3WriteProtect, "DF3 write-protect", Toggle), ]; -const STORAGE_ROWS: [Row; 11] = [ +const STORAGE_ROWS: [Row; 12] = [ row(F::IdeMaster, "IDE master", Drive), row(F::IdeSlave, "IDE slave", Drive), + row(F::ScsiController, "SCSI controller", Cycle), row(F::ScsiRom, "SCSI boot ROM", PathRow), row(F::ScsiRomOdd, "SCSI ROM (odd)", PathRow), row(F::ScsiUnit0, "SCSI unit 0", Drive), @@ -497,6 +499,13 @@ const WARPS: [WarpSpeed; 5] = [ // The stepper flips the two explicit modes, matching the runtime toggle. const JOYSTICK_MODES: [JoystickInputMode; 2] = [JoystickInputMode::Gamepad, JoystickInputMode::Keyboard]; +// `None` = no SCSI board fitted; the two boards are mutually exclusive here even +// though the engine could run both, so a config round-trips through this picker. +const SCSI_CONTROLLERS: [Option; 3] = [ + None, + Some(ScsiController::A2091), + Some(ScsiController::A4091), +]; #[cfg(feature = "midi")] const SERIAL_MODES: [SerialMode; 4] = [ SerialMode::Off, @@ -552,6 +561,9 @@ pub struct MachineSetup { ide_master_name: Option, ide_slave: Option, ide_slave_name: Option, + /// Which SCSI host adapter is fitted, or `None` for no board. Shares the + /// `scsi_*` ROM/unit block below (the drives are portable between boards). + scsi_controller: Option, scsi_rom: Option, scsi_rom_odd: Option, scsi_units: [Option; 7], @@ -645,6 +657,7 @@ impl MachineSetup { ide_master_name: cfg.ide.master.as_ref().and_then(|d| d.volume_name.clone()), ide_slave: cfg.ide.slave.as_ref().map(|d| d.path.clone()), ide_slave_name: cfg.ide.slave.as_ref().and_then(|d| d.volume_name.clone()), + scsi_controller: cfg.scsi.enabled().then_some(cfg.scsi.controller), scsi_rom: cfg.scsi.rom.clone(), scsi_rom_odd: cfg.scsi.rom_odd.clone(), scsi_units: std::array::from_fn(|i| cfg.scsi.units[i].as_ref().map(|d| d.path.clone())), @@ -818,36 +831,48 @@ impl MachineSetup { // Hard disk raw.ide.master = drive_raw(self.ide_master.as_deref(), self.ide_master_name.as_deref()); raw.ide.slave = drive_raw(self.ide_slave.as_deref(), self.ide_slave_name.as_deref()); - raw.scsi.rom = self.scsi_rom.as_deref().map(path_string); - raw.scsi.rom_odd = self.scsi_rom_odd.as_deref().map(path_string); - raw.scsi.unit0 = drive_raw( - self.scsi_units[0].as_deref(), - self.scsi_unit_names[0].as_deref(), - ); - raw.scsi.unit1 = drive_raw( - self.scsi_units[1].as_deref(), - self.scsi_unit_names[1].as_deref(), - ); - raw.scsi.unit2 = drive_raw( - self.scsi_units[2].as_deref(), - self.scsi_unit_names[2].as_deref(), - ); - raw.scsi.unit3 = drive_raw( - self.scsi_units[3].as_deref(), - self.scsi_unit_names[3].as_deref(), - ); - raw.scsi.unit4 = drive_raw( - self.scsi_units[4].as_deref(), - self.scsi_unit_names[4].as_deref(), - ); - raw.scsi.unit5 = drive_raw( - self.scsi_units[5].as_deref(), - self.scsi_unit_names[5].as_deref(), - ); - raw.scsi.unit6 = drive_raw( - self.scsi_units[6].as_deref(), - self.scsi_unit_names[6].as_deref(), - ); + // Only emit `[scsi]` when a controller is fitted, so an unset board + // leaves the section absent rather than writing dangling ROM/units. + if let Some(controller) = self.scsi_controller { + // "a2091" is the default, so omit it; name only "a4091". + raw.scsi.controller = match controller { + ScsiController::A2091 => None, + ScsiController::A4091 => Some("a4091".to_string()), + }; + raw.scsi.rom = self.scsi_rom.as_deref().map(path_string); + // rom_odd is an A2091 split-EPROM option; the A4091 has one ROM. + raw.scsi.rom_odd = (controller == ScsiController::A2091) + .then(|| self.scsi_rom_odd.as_deref().map(path_string)) + .flatten(); + raw.scsi.unit0 = drive_raw( + self.scsi_units[0].as_deref(), + self.scsi_unit_names[0].as_deref(), + ); + raw.scsi.unit1 = drive_raw( + self.scsi_units[1].as_deref(), + self.scsi_unit_names[1].as_deref(), + ); + raw.scsi.unit2 = drive_raw( + self.scsi_units[2].as_deref(), + self.scsi_unit_names[2].as_deref(), + ); + raw.scsi.unit3 = drive_raw( + self.scsi_units[3].as_deref(), + self.scsi_unit_names[3].as_deref(), + ); + raw.scsi.unit4 = drive_raw( + self.scsi_units[4].as_deref(), + self.scsi_unit_names[4].as_deref(), + ); + raw.scsi.unit5 = drive_raw( + self.scsi_units[5].as_deref(), + self.scsi_unit_names[5].as_deref(), + ); + raw.scsi.unit6 = drive_raw( + self.scsi_units[6].as_deref(), + self.scsi_unit_names[6].as_deref(), + ); + } // CD raw.cd.image = self.cd_image.as_deref().map(path_string); if self.cd_insert_delay != 0.0 { @@ -1038,6 +1063,20 @@ impl MachineSetup { F::Dcache => reason(self.cpu.has_data_cache(), "needs 68030/040"), F::Z3Ram => reason(cpu_is_32bit(self.cpu), "needs 32-bit CPU"), F::IdeMaster | F::IdeSlave => reason(self.has_gayle(), "needs A600/A1200"), + // The ROM and drives belong to the fitted controller; greyed with + // none. rom_odd is an A2091 split-EPROM option only. + F::ScsiRom + | F::ScsiUnit0 + | F::ScsiUnit1 + | F::ScsiUnit2 + | F::ScsiUnit3 + | F::ScsiUnit4 + | F::ScsiUnit5 + | F::ScsiUnit6 => reason(self.scsi_controller.is_some(), "no controller"), + F::ScsiRomOdd => reason( + self.scsi_controller == Some(ScsiController::A2091), + "A2091 only", + ), F::CdImage | F::CdInsertDelay => reason(self.has_cd(), "needs CDTV/CD32"), F::Cd32Nvram => reason(self.model == Some(MachineModel::Cd32), "CD32 only"), F::Df0Image | F::Df0WriteProtect => reason(self.floppy_drives >= 1, "drive off"), @@ -1214,6 +1253,11 @@ impl MachineSetup { JoystickInputMode::Keyboard => "Keyboard".to_string(), JoystickInputMode::Gamepad => "Gamepad".to_string(), }, + F::ScsiController => match self.scsi_controller { + None => "None".to_string(), + Some(ScsiController::A2091) => "A2091 (Z2)".to_string(), + Some(ScsiController::A4091) => "A4091 (Z3)".to_string(), + }, #[cfg(feature = "midi")] F::SerialMode => match self.serial_mode { SerialMode::Off => "Off".to_string(), @@ -1309,6 +1353,9 @@ impl MachineSetup { self.joystick_input_mode = cycle_slice(&JOYSTICK_MODES, self.joystick_input_mode, forward) } + F::ScsiController => { + self.scsi_controller = cycle_slice(&SCSI_CONTROLLERS, self.scsi_controller, forward) + } #[cfg(feature = "midi")] F::SerialMode => { self.serial_mode = cycle_slice(&SERIAL_MODES, self.serial_mode, forward)