From 437d1c6ded3dabb08ac8351a30292ab548c30ce9 Mon Sep 17 00:00:00 2001 From: Reza Fouladian <9205977+rezafouladian@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:30:38 -0700 Subject: [PATCH 1/4] Add SLIM card support Add basic SLIM card support for the Macintosh Portable --- core/src/emulator/comm.rs | 11 ++ core/src/emulator/mod.rs | 82 +++++++++- core/src/mac/mod.rs | 4 + core/src/mac/portable/bus.rs | 54 ++++++- core/src/mac/portable/normandy.rs | 195 +++++++++++++++++++++-- core/src/mac/scsi/disk_image.rs | 2 +- frontend_egui/src/app.rs | 130 +++++++++++++-- frontend_egui/src/dialogs/modelselect.rs | 46 +++++- frontend_egui/src/emulator.rs | 36 ++++- frontend_egui/src/settings.rs | 2 + frontend_egui/src/workspace.rs | 39 +++++ 11 files changed, 564 insertions(+), 37 deletions(-) diff --git a/core/src/emulator/comm.rs b/core/src/emulator/comm.rs index 2feaa514..b3bd3b05 100644 --- a/core/src/emulator/comm.rs +++ b/core/src/emulator/comm.rs @@ -43,6 +43,8 @@ pub enum EmulatorCommand { /// Toggle automatic writeback for a floppy drive. /// Parameters: drive id, enabled SetFloppyWriteback(usize, bool), + SlimInsert(usize, PathBuf, bool), + SlimEject(usize), ScsiAttachHdd(usize, PathBuf), ScsiBranchHdd(usize, PathBuf), ScsiAttachCdrom(usize), @@ -134,6 +136,8 @@ pub struct EmulatorStatus { pub fdd: [FddStatus; 3], pub model: MacModel, pub has_pmmu: bool, + pub has_slim: bool, + pub slim: [Option; 2], pub speed: EmulatorSpeed, pub effective_speed: f64, pub scsi: [Option; 7], @@ -178,6 +182,13 @@ pub struct FddStatus { pub writeback_enabled: bool, } +#[derive(Debug, Clone)] +pub struct SlimSlotStatus { + pub image: PathBuf, + pub size: usize, + pub write_protect: bool, +} + /// A friendly message ready for display to a user #[derive(Debug)] pub enum UserMessageType { diff --git a/core/src/emulator/mod.rs b/core/src/emulator/mod.rs index a11f896a..58765c87 100644 --- a/core/src/emulator/mod.rs +++ b/core/src/emulator/mod.rs @@ -28,6 +28,7 @@ use crate::mac::compact::bus::{CompactMacBus, RAM_DIRTY_PAGESIZE}; use crate::mac::macii::bus::{DEFAULT_BUS_SPEED, MacIIBus}; use crate::mac::portable::bus::MacPortableBus; use crate::mac::scc::Scc; +use crate::mac::scsi::disk_image::FileDiskImage; use crate::mac::scsi::target::ScsiTargetEvent; use crate::mac::serial_bridge::{SccBridge, SerialBridgeStatus}; use crate::mac::swim::drive::DriveType; @@ -51,7 +52,7 @@ use crate::mac::scsi::disk_image::DiskImage; use crate::mac::swim::Swim; use comm::{ Breakpoint, EmulatorCommand, EmulatorCommandSender, EmulatorEvent, EmulatorEventReceiver, - EmulatorStatus, FddStatus, InputRecording, ScsiTargetStatus, + EmulatorStatus, FddStatus, InputRecording, ScsiTargetStatus, SlimSlotStatus, }; /// Mouse emulation mode @@ -237,6 +238,44 @@ dispatch! { } } +impl EmulatorConfig { + /// Returns true if a SLIM card adapter is installed + fn has_slim(&self) -> bool { + match self { + Self::Portable(cpu) => cpu.bus.slim_installed(), + _ => false, + } + } + + /// Returns the status of the SLIM card slots + fn slim_status(&self) -> [Option; 2] { + match self { + Self::Portable(cpu) => cpu.bus.slim_status(), + _ => Default::default(), + } + } + + /// Inserts a SLIM card image into a slot + fn slim_insert(&mut self, slot: usize, filename: &Path, write_protect: bool) -> Result<()> { + match self { + Self::Portable(cpu) => { + cpu.bus.slim_eject(slot)?; + let image = FileDiskImage::open(filename, !write_protect)?; + cpu.bus.slim_insert(slot, Box::new(image), write_protect) + } + _ => bail!("SLIM cards not supported on this model"), + } + } + + /// Ejects the SLIM card from a slot + fn slim_eject(&mut self, slot: usize) -> Result<()> { + match self { + Self::Portable(cpu) => cpu.bus.slim_eject(slot), + _ => bail!("SLIM cards not supported on this model"), + } + } +} + /// An interface that allows sub-components to access emulator state (such as the speed setting) pub trait EmuContext { fn speed(&self) -> EmulatorSpeed; @@ -276,6 +315,7 @@ impl Emulator { None, None, false, + false, None, ) } @@ -290,6 +330,7 @@ impl Emulator { ram_size: Option, override_fdd_type: Option, pmmu_enabled: bool, + slim_adapter: bool, shared_dir: Option, ) -> Result<(Self, Arc>>)> { // Set up channels @@ -331,6 +372,11 @@ impl Emulator { } MacModel::Portable | MacModel::Portable15MB => { assert!(!pmmu_enabled, "PMMU not available on compact models"); + assert!( + !slim_adapter + || ram_size.unwrap_or_else(|| model.ram_size_default()) <= 5 * 1024 * 1024, + "RAM size cannot be greater than 5MB when SLIM adapter is enabled!" + ); // Find extension ROM if present let extension_rom = extra_roms.iter().find_map(|p| match p { @@ -339,8 +385,15 @@ impl Emulator { }); // Initialize bus and CPU - let bus = - MacPortableBus::new(model, rom, extension_rom, renderer, mouse_mode, ram_size); + let bus = MacPortableBus::new( + model, + rom, + extension_rom, + renderer, + mouse_mode, + ram_size, + slim_adapter, + ); let cpu = Box::new(CpuM68000::new(bus)); assert_eq!(cpu.get_type(), model.cpu_type()); @@ -641,6 +694,8 @@ impl Emulator { }), model: self.model, has_pmmu: self.config.cpu_has_pmmu(), + has_slim: self.config.has_slim(), + slim: self.config.slim_status(), scsi: core::array::from_fn(|i| { self.config .scsi() @@ -1037,6 +1092,27 @@ impl Tickable for Emulator { } self.status_update()?; } + EmulatorCommand::SlimInsert(slot, filename, write_protect) => { + match self.config.slim_insert(slot, &filename, write_protect) { + Ok(()) => { + info!( + "SLIM slot {}: card inserted, image '{}' loaded", + slot + 1, + filename.display() + ); + } + Err(e) => { + self.user_error(&format!("SLIM slot {}: {:#}", slot + 1, e)); + } + } + self.status_update()?; + } + EmulatorCommand::SlimEject(slot) => { + if let Err(e) = self.config.slim_eject(slot) { + self.user_error(&format!("SLIM slot {}: {:#}", slot + 1, e)); + } + self.status_update()?; + } EmulatorCommand::ScsiAttachHdd(id, filename) => { match self.load_hdd_image(&filename, id) { Ok(_) => { diff --git a/core/src/mac/mod.rs b/core/src/mac/mod.rs index 7327a50a..626332c0 100644 --- a/core/src/mac/mod.rs +++ b/core/src/mac/mod.rs @@ -113,6 +113,10 @@ impl MacModel { } } + pub const fn has_slim(self) -> bool { + matches!(self, Self::Portable) + } + /// Default RAM size pub const fn ram_size_default(self) -> usize { match self { diff --git a/core/src/mac/portable/bus.rs b/core/src/mac/portable/bus.rs index 2c134816..b94cd568 100644 --- a/core/src/mac/portable/bus.rs +++ b/core/src/mac/portable/bus.rs @@ -7,7 +7,7 @@ use super::via::Via; use super::video::Video; use crate::bus::{Address, Bus, BusMember, BusResult, InspectableBus, IrqSource}; use crate::debuggable::Debuggable; -use crate::emulator::comm::EmulatorSpeed; +use crate::emulator::comm::{EmulatorSpeed, SlimSlotStatus}; use crate::emulator::{EmuContext, MouseMode}; use crate::keymap::KeyEvent; use crate::mac::MacModel; @@ -17,6 +17,7 @@ use crate::mac::macii::bus::DEFAULT_BUS_SPEED; use crate::mac::rtc::Rtc; use crate::mac::scc::Scc; use crate::mac::scsi::controller::ScsiController; +use crate::mac::scsi::disk_image::DiskImage; use crate::mac::swim::Swim; use crate::renderer::{ AUDIO_BUFFER_SAMPLES, AUDIO_CHANNELS, AudioProvider, Renderer, null_audio_sink, @@ -108,6 +109,7 @@ where renderer: TRenderer, mouse_mode: MouseMode, ram_size: Option, + slim_adapter: bool, ) -> Self { let ram_size = ram_size.unwrap_or_else(|| model.ram_size_default()); @@ -161,7 +163,7 @@ where progkey_pressed: LatchingEvent::default(), mouse_mode, pmgr: Pmgr::new(), - normandy: Normandy::new(), + normandy: Normandy::new(slim_adapter), }; // Disable memory test @@ -179,6 +181,7 @@ where /// Reinstalls things that can't be serialized and does some updates upon deserialization pub fn after_deserialize(&mut self, _renderer: TRenderer) { self.asc.after_deserialize(); + self.normandy.after_deserialize(); // Mark all RAM pages as dirty after deserialization to update memory display self.ram_dirty @@ -189,6 +192,31 @@ where self.model } + /// Returns true if the SLIM card adapter is installed + pub fn slim_installed(&self) -> bool { + self.normandy.slim_installed() + } + + /// Returns the status of the SLIM card slots + pub fn slim_status(&self) -> [Option; 2] { + self.normandy.slim_status() + } + + /// Inserts a SLIM card image into a slot + pub fn slim_insert( + &mut self, + slot: usize, + image: Box, + write_protect: bool, + ) -> Result<()> { + self.normandy.slim_insert(slot, image, write_protect) + } + + /// Ejects the SLIM card from a slot + pub fn slim_eject(&mut self, slot: usize) -> Result<()> { + self.normandy.slim_eject(slot) + } + pub fn get_effective_speed(&self) -> f64 { self.pmgr.rtc.effective_speed() } @@ -238,7 +266,15 @@ where fn write_normal(&mut self, addr: Address, val: Byte) -> Option<()> { match addr { // RAM - 0x0000_0000..=0x008F_FFFF => self.normal_ram_write(addr, val), + 0x0000_0000..=0x004F_FFFF => self.normal_ram_write(addr, val), + // SLIM cards, or RAM + 0x0050_0000..=0x008F_FFFF => { + if self.normandy.slim_installed() { + self.normandy.write(addr, val) + } else { + self.normal_ram_write(addr, val) + } + } // ROM, or Remapped RAM for 15MB RAM Mod 0x0090_0000..=0x009F_FFFF => { if self.model == MacModel::Portable15MB { @@ -311,7 +347,16 @@ where fn read_normal(&mut self, addr: Address) -> Option { match addr { - 0x0000_0000..=0x008F_FFFF => self.normal_ram_read(addr), + // RAM + 0x0000_0000..=0x004F_FFFF => self.normal_ram_read(addr), + // SLIM cards, or RAM + 0x0050_0000..=0x008F_FFFF => { + if self.normandy.slim_installed() { + self.normandy.read(addr).or(Some(Self::OPENBUS)) + } else { + self.normal_ram_read(addr) + } + } // ROM, or Remapped RAM for 15MB RAM Mod 0x0090_0000..=0x009F_FFFF => { if self.model == MacModel::Portable15MB { @@ -589,6 +634,7 @@ where self.via.b_in.set_pmack(self.pmgr.pmack); self.via.a_in.0 = self.pmgr.a_in; self.via.ifr.set_pmgr(self.pmgr.interrupt); + self.normandy.tick(ticks, ())?; Ok(()) } diff --git a/core/src/mac/portable/normandy.rs b/core/src/mac/portable/normandy.rs index d4f0ff1c..d528e148 100644 --- a/core/src/mac/portable/normandy.rs +++ b/core/src/mac/portable/normandy.rs @@ -2,11 +2,14 @@ //! Also known as the "CPU GLU" or "Coarse Address Decode and GLU". //! Aside from the address decoding duties, this chip also handled mapping and timing for the //! SLIM card system. +//! SLIM card adapter support has also been included, though the SLIM hardware never released. use crate::bus::{Address, BusMember}; use crate::debuggable::{Debuggable, DebuggableProperties}; +use crate::emulator::comm::SlimSlotStatus; +use crate::mac::scsi::disk_image::DiskImage; use crate::tickable::{Tickable, Ticks}; -use anyhow::Result; +use anyhow::{Result, bail}; use proc_bitfield::bitfield; use serde::{Deserialize, Serialize}; @@ -35,7 +38,7 @@ bitfield! { bitfield! { #[derive(Serialize, Deserialize)] struct SlimStatus(u8): { - /// SLIM card is read-only + /// SLIM card is read-only/write protected readonly: bool @ 2, /// SLIM card is inserted inserted: bool @ 3, @@ -53,17 +56,23 @@ bitfield! { bitfield! { #[derive(Serialize, Deserialize)] struct SlimProtect(u8): { - /// SLIM card is write protected + /// SLIM card is software write protected protect: bool @ 3, } } +#[derive(Default)] +struct SlimCard { + image: Option>, + write_protect: bool, +} + #[derive(Serialize, Deserialize)] #[serde(bound = "")] pub struct Normandy { // Idle speed register pub idle_speed: bool, - // SLIM DTACK loag register + // SLIM DTACK load register pub slim_dtack: bool, slim_mapper: Vec, @@ -76,31 +85,128 @@ pub struct Normandy { slim2_protect: SlimProtect, slim_rom: Vec, + #[serde(skip)] + cards: [SlimCard; 2], pub dtack_counter: u8, } impl Normandy { - pub(crate) fn new() -> Self { + pub(crate) fn new(slim: bool) -> Self { Self { idle_speed: false, slim_dtack: false, slim_mapper: vec![SlimMapper(0); 16], - slim_adapter: SlimAdapter(0x00), - slim1_status: SlimStatus(0x08), - slim1_eject: SlimEject(0x08), + slim_adapter: SlimAdapter(0x00).with_installed(slim), + slim1_status: SlimStatus(0x00), + slim1_eject: SlimEject(0x00).with_eject(true), slim1_protect: SlimProtect(0), - slim2_status: SlimStatus(0x08), - slim2_eject: SlimEject(0x08), + slim2_status: SlimStatus(0x00), + slim2_eject: SlimEject(0x00).with_eject(true), slim2_protect: SlimProtect(0), slim_rom: vec![0; 0x10000], + cards: Default::default(), dtack_counter: 0, } } + /// Card images aren't serialized, so report both slots as empty after loading a state + pub(crate) fn after_deserialize(&mut self) { + self.slim1_status.set_inserted(false); + self.slim1_status.set_readonly(false); + self.slim2_status.set_inserted(false); + self.slim2_status.set_readonly(false); + } + + pub(crate) fn slim_installed(&self) -> bool { + self.slim_adapter.installed() + } + + pub(crate) fn slim_status(&self) -> [Option; 2] { + core::array::from_fn(|i| { + let card = &self.cards[i]; + let image = card.image.as_ref()?; + Some(SlimSlotStatus { + image: image.image_path()?.to_path_buf(), + size: image.byte_len(), + write_protect: card.write_protect, + }) + }) + } + + pub(crate) fn slim_insert( + &mut self, + slot: usize, + image: Box, + write_protect: bool, + ) -> Result<()> { + if !self.slim_installed() { + bail!("SLIM card adapter not installed"); + } + if slot >= self.cards.len() { + bail!("Invalid SLIM slot {}", slot); + } + match slot { + 0 => { + self.slim1_status.set_inserted(true); + self.slim1_eject.set_eject(true); + self.slim1_status.set_readonly(write_protect); + } + _ => { + self.slim2_status.set_inserted(true); + self.slim2_eject.set_eject(true); + self.slim2_status.set_readonly(write_protect); + } + } + self.cards[slot] = SlimCard { + image: Some(image), + write_protect, + }; + Ok(()) + } + + pub(crate) fn slim_eject(&mut self, slot: usize) -> Result<()> { + if slot >= self.cards.len() { + bail!("Invalid SLIM slot {}", slot); + } + self.cards[slot].image = None; + match slot { + 0 => { + self.slim1_eject.set_eject(false); + } + _ => { + self.slim2_eject.set_eject(false); + } + } + Ok(()) + } + + fn slim_read(&self, slot: usize, offset: usize) -> Option { + let image = self.cards[slot].image.as_ref()?; + if offset >= image.byte_len() { + return None; + } + match image.media_bytes() { + Some(bytes) => Some(bytes[offset]), + None => Some(image.read_bytes(offset, 1)[0]), + } + } + + fn slim_write(&mut self, slot: usize, offset: usize, val: u8) { + let card = &mut self.cards[slot]; + if card.write_protect { + return; + } + if let Some(image) = card.image.as_mut() + && offset < image.byte_len() + { + image.write_bytes(offset, &[val]); + } + } + pub(crate) fn waitstate(&mut self, addr: Address) -> bool { match addr { 0x0000_0000..=0x008F_FFFF => { @@ -119,7 +225,7 @@ impl Normandy { true } } - } else if !self.slim_dtack & (0x0050_0000..=0x0050_FFFF).contains(&addr) { + } else if !self.slim_dtack && (0x0050_0000..=0x008F_FFFF).contains(&addr) { match self.dtack_counter { 0 => { self.dtack_counter = SLIM_DTACK_DELAY; @@ -146,8 +252,13 @@ impl Normandy { impl BusMember
for Normandy { fn read(&mut self, addr: Address) -> Option { match addr { + // SLIM card 1 space + 0x50_0000..=0x6F_FFFF => self.slim_read(0, (addr - 0x50_0000) as usize), + // SLIM card 2 space + 0x70_0000..=0x8F_FFFF => self.slim_read(1, (addr - 0x70_0000) as usize), // SLIM adapter ROM 0xE0_0000..=0xE0_FFFF => Some(0x00), + // SLIM adapter registers 0xF0_0000..=0xF0_FFFF => { if self.slim_adapter.installed() { match addr { @@ -167,6 +278,7 @@ impl BusMember
for Normandy { None } } + // Normandy SLIM registers 0xFC_0000..=0xFC_FFFF => match addr & 0x21F { 0x000..=0x01F => { if addr & 0x1 != 0 { @@ -177,11 +289,7 @@ impl BusMember
for Normandy { } 0x200..=0x201 => { self.slim_dtack = true; - if self.slim_adapter.installed() { - Some(0x08) - } else { - Some(0x00) - } + Some(self.slim_adapter.0) } 0x202..=0x203 => Some(0x00), _ => None, @@ -204,7 +312,41 @@ impl BusMember
for Normandy { fn write(&mut self, addr: Address, val: u8) -> Option<()> { match addr { - 0xF0_0000..=0xF0_FFFF => Some(()), + // SLIM card 1 space + 0x50_0000..=0x6F_FFFF => { + self.slim_write(0, (addr - 0x50_0000) as usize, val); + Some(()) + } + // SLIM card 2 space + 0x70_0000..=0x8F_FFFF => { + self.slim_write(1, (addr - 0x70_0000) as usize, val); + Some(()) + } + // SLIM adapter registers + 0xF0_0000..=0xF0_FFFF => match addr { + 0xF0_0010 => Some(()), + 0xF0_0011 => { + self.slim1_eject.set_eject(SlimEject(val).eject()); + Some(()) + } + 0xF0_0020 => Some(()), + 0xF0_0021 => { + self.slim1_protect.set_protect(SlimProtect(val).protect()); + Some(()) + } + 0xF0_0040 => Some(()), + 0xF0_0041 => { + self.slim2_eject.set_eject(SlimEject(val).eject()); + Some(()) + } + 0xF0_0050 => Some(()), + 0xF0_0051 => { + self.slim2_protect.set_protect(SlimProtect(val).protect()); + Some(()) + } + _ => None, + }, + // Normandy SLIM registers 0xFC_0000..=0xFC_FFFF => match addr & 0x21F { 0x000..=0x01F => { if addr & 0x1 != 0 { @@ -238,6 +380,16 @@ impl BusMember
for Normandy { impl Tickable for Normandy { fn tick(&mut self, ticks: Ticks, _: ()) -> Result { + if !self.slim1_eject.eject() && self.slim1_status.inserted() { + self.slim_eject(0)?; + self.slim1_status.set_inserted(false); + self.slim1_status.set_readonly(false); + } + if !self.slim2_eject.eject() && self.slim2_status.inserted() { + self.slim_eject(1)?; + self.slim2_status.set_inserted(false); + self.slim2_status.set_readonly(false); + } Ok(ticks) } } @@ -250,6 +402,15 @@ impl Debuggable for Normandy { vec![ dbgprop_bool!("Idle", self.idle_speed), dbgprop_bool!("Slim DTACK", self.slim_dtack), + dbgprop_bool!("Slim Adapter Installed", self.slim_adapter.installed()), + dbgprop_bool!("Slim 1 Inserted", self.slim1_status.inserted()), + dbgprop_bool!("Slim 2 Inserted", self.slim2_status.inserted()), + dbgprop_bool!("Slim 1 Read Only", self.slim1_status.readonly()), + dbgprop_bool!("Slim 2 Read Only", self.slim2_status.readonly()), + dbgprop_bool!("Slim 1 Protected", self.slim1_protect.protect()), + dbgprop_bool!("Slim 2 Protected", self.slim2_protect.protect()), + dbgprop_bool!("Slim 1 Ejecting", !self.slim1_eject.eject()), + dbgprop_bool!("Slim 2 Ejecting", !self.slim2_eject.eject()), ] } } diff --git a/core/src/mac/scsi/disk_image.rs b/core/src/mac/scsi/disk_image.rs index 2c41c6a3..da298cb8 100644 --- a/core/src/mac/scsi/disk_image.rs +++ b/core/src/mac/scsi/disk_image.rs @@ -29,7 +29,7 @@ pub(crate) struct FileDiskImage { } impl FileDiskImage { - pub(super) fn open(filename: &Path, writable: bool) -> Result { + pub(crate) fn open(filename: &Path, writable: bool) -> Result { Self::open_file(filename, writable) } diff --git a/frontend_egui/src/app.rs b/frontend_egui/src/app.rs index 6d0e248a..1bf7a8e3 100644 --- a/frontend_egui/src/app.rs +++ b/frontend_egui/src/app.rs @@ -2,7 +2,7 @@ use crate::dialogs::about::AboutDialog; use crate::dialogs::confirm::ConfirmDialog; use crate::dialogs::diskimage::{DiskImageDialog, DiskImageDialogResult}; use crate::dialogs::filedialog::SnowFileDialog; -use crate::dialogs::modelselect::{ModelSelectionDialog, ModelSelectionResult}; +use crate::dialogs::modelselect::{ModelSelectionDialog, ModelSelectionResult, format_ram}; use crate::emulator::EmulatorState; use crate::emulator::{EmulatorInitArgs, ScsiTargets}; use crate::keymap::{char_to_keystroke, map_winit_keycode}; @@ -210,6 +210,10 @@ pub struct SnowGui { floppy_dialog_last_type: Option, floppy_dialog_target: FloppyDialogTarget, floppy_dialog_wp: bool, + slim_dialog: SnowFileDialog, + slim_dialog_idx: usize, + slim_dialog_wp: bool, + slim_blank_size: Option, create_disk_dialog: DiskImageDialog, record_dialog: SnowFileDialog, model_dialog: ModelSelectionDialog, @@ -274,6 +278,7 @@ impl SnowGui { const ZOOM_FACTORS: [f32; 8] = [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 4.0]; const SUBMENU_WIDTH: f32 = 175.0; const DYNAMIC_FF_RESUME_MS: u64 = 500; + const SLIM_CARD_SIZES: &[usize; 3] = &[512 * 1024, 1024 * 1024, 2048 * 1024]; /// Returns the target fast-forward speed based on current limit settings. fn ff_target_speed(&self) -> EmulatorSpeed { @@ -442,6 +447,16 @@ impl SnowGui { floppy_dialog_last_image: None, floppy_dialog_last_type: None, floppy_dialog_wp: false, + slim_dialog: SnowFileDialog::new() + .add_filter("SLIM card images", &["slim", "bin"]) + .add_save_extension("SLIM card image", "slim") + .default_save_extension("SLIM card image") + .opening_mode(egui_file_dialog::OpeningMode::LastVisitedDir) + .initial_directory(Self::default_dir()) + .storage(settings.fd_slim), + slim_dialog_idx: 0, + slim_dialog_wp: false, + slim_blank_size: None, workspace_dialog: SnowFileDialog::new() .add_filter("Snow workspace", &["snoww"]) .add_save_extension("Snow workspace", "snoww") @@ -717,19 +732,22 @@ impl SnowGui { whole.to_pos2() } - fn try_create_image(&self, result: &DiskImageDialogResult) -> Result<()> { - if result.filename.try_exists()? { + fn create_blank_file(path: &Path, size: usize) -> Result<()> { + if path.try_exists()? { bail!( "Cowardly refusing to overwrite existing file. Delete the file first, or choose a different filename." ); } - { - let mut file = File::create(result.filename.clone())?; - file.seek(SeekFrom::Start(result.size as u64 - 1))?; - file.write_all(&[0])?; - file.flush()?; - } + let mut file = File::create(path)?; + file.seek(SeekFrom::Start(size as u64 - 1))?; + file.write_all(&[0])?; + file.flush()?; + Ok(()) + } + + fn try_create_image(&self, result: &DiskImageDialogResult) -> Result<()> { + Self::create_blank_file(&result.filename, result.size)?; self.emu.scsi_attach_hdd(result.scsi_id, &result.filename); Ok(()) } @@ -892,6 +910,11 @@ impl SnowGui { ui.set_min_width(Self::SUBMENU_WIDTH); self.draw_menu_floppies(ui); + if self.emu.has_slim() { + ui.separator(); + self.draw_menu_slim(ui); + } + // Needs cloning for the later borrow to call create_disk_dialog.open() let targets = self.emu.get_scsi_target_status().map(|d| d.to_owned()); if let Some(targets) = targets { @@ -2064,6 +2087,58 @@ impl SnowGui { } } + fn draw_menu_slim(&mut self, ui: &mut egui::Ui) { + let Some(slots) = self.emu.get_slim_status().map(|s| s.to_owned()) else { + return; + }; + for (i, slot) in slots.iter().enumerate() { + let title = match slot { + None => format!( + "{} SLIM slot #{}: (empty)", + egui_material_icons::icons::ICON_EJECT, + i + 1 + ), + Some(s) => format!( + "{} SLIM slot #{}: {}", + if s.write_protect { + egui_material_icons::icons::ICON_LOCK + } else { + egui_material_icons::icons::ICON_SD_CARD + }, + i + 1, + s.image.file_name().unwrap_or_default().to_string_lossy(), + ), + }; + ui.menu_button(title, |ui| { + ui.set_min_width(Self::SUBMENU_WIDTH); + if ui.button("Load image...").clicked() { + self.slim_dialog_idx = i; + self.slim_blank_size = None; + self.slim_dialog + .pick_file(self.settings.native_file_dialogs); + } + ui.menu_button("Create blank image", |ui| { + for &sz in Self::SLIM_CARD_SIZES { + if ui.button(format_ram(sz)).clicked() { + self.slim_dialog_idx = i; + self.slim_blank_size = Some(sz); + self.slim_dialog + .save_file(self.settings.native_file_dialogs); + } + } + }); + ui.checkbox(&mut self.slim_dialog_wp, "Mount write-protected"); + ui.separator(); + if ui + .add_enabled(slot.is_some(), egui::Button::new("Force eject")) + .clicked() + { + self.emu.slim_eject(i); + } + }); + } + } + fn draw_fast_forward_button(&self, ui: &mut egui::Ui) -> egui::Response { let is_active = self.ff_on; let selection_color = ui.visuals().selection.stroke.color; @@ -2720,6 +2795,14 @@ impl SnowGui { )); } + if init_args.slim_adapter { + for (slot, card) in self.workspace.slim_cards().into_iter().enumerate() { + if let Some((image, write_protect)) = card { + self.emu.slim_insert(slot, &image, write_protect); + } + } + } + #[cfg(feature = "ethernet")] if let Some(scsi) = self.emu.get_scsi_targets() && let Some((id, _)) = scsi @@ -2754,6 +2837,12 @@ impl SnowGui { self.workspace.set_scsi_target(i, d.clone()); } } + if let Some(slim_cards) = self.emu.get_slim_status() { + for slot in 0..2 { + self.workspace + .set_slim_card(slot, slim_cards.get(slot).cloned().flatten()); + } + } if let Err(e) = self.workspace.write_file(path) { self.show_error(&format!("Failed to save workspace: {}", e)); } @@ -3578,6 +3667,29 @@ impl eframe::App for SnowGui { } self.ui_active &= *self.floppy_dialog.state() != egui_file_dialog::DialogState::Open; + // SLIM card image picker dialog + self.slim_dialog.update(ctx, frame); + if let Some(path) = self.slim_dialog.take_picked() { + let slot = self.slim_dialog_idx; + match self.slim_dialog.mode() { + DialogMode::PickFile => { + self.emu.slim_insert(slot, &path, self.slim_dialog_wp); + } + DialogMode::SaveFile => { + if let Some(size) = self.slim_blank_size.take() { + match Self::create_blank_file(&path, size) { + Ok(()) => self.emu.slim_insert(slot, &path, false), + Err(e) => self.show_error(&format!("Failed to create image: {}", e)), + } + } + } + _ => unreachable!(), + } + self.settings.fd_slim = self.slim_dialog.storage_mut().clone(); + self.settings.save(); + } + self.ui_active &= *self.slim_dialog.state() != egui_file_dialog::DialogState::Open; + // HDD image picker dialog self.hdd_dialog.update(ctx, frame); if let Some(path) = self.hdd_dialog.take_picked() { diff --git a/frontend_egui/src/dialogs/modelselect.rs b/frontend_egui/src/dialogs/modelselect.rs index f9be2349..fdbffb44 100644 --- a/frontend_egui/src/dialogs/modelselect.rs +++ b/frontend_egui/src/dialogs/modelselect.rs @@ -19,6 +19,9 @@ const SELECTABLE_VIDEO_CARDS: &[NubusDeviceKind] = &[NubusDeviceKind::Mdc12, Nub /// Video cards that support a user-selectable monitor. const VIDEO_CARDS_WITH_MONITOR: &[NubusDeviceKind] = &[NubusDeviceKind::Mdc12]; +/// Maximum RAM size supported with the SLIM card adapter installed +const SLIM_ADAPTER_MAX_RAM: usize = 5 * 1024 * 1024; + /// Dialog for selecting Macintosh model and associated ROMs pub struct ModelSelectionDialog { open: bool, @@ -67,7 +70,7 @@ pub struct ModelSelectionDialog { display_rom_error: String, } -fn format_ram(sz: usize) -> String { +pub fn format_ram(sz: usize) -> String { if sz < 1024 * 1024 { format!("{} KB", sz / 1024) } else { @@ -171,6 +174,10 @@ impl ModelSelectionDialog { } self.early_800k = init_args.override_fdd_type == Some(DriveType::GCR800KPWM); self.init_args = init_args.clone(); + if !self.selected_model.has_slim() { + self.init_args.slim_adapter = false; + } + self.clamp_ram_for_slim_adapter(); // Recompute model-dependent state (e.g. whether a display ROM is required). self.update_display_rom_requirement(); @@ -201,6 +208,10 @@ impl ModelSelectionDialog { self.update_display_rom_requirement(); self.init_args.ram_size = None; + if !self.selected_model.has_slim() { + self.init_args.slim_adapter = false; + } + self.clamp_ram_for_slim_adapter(); // Load from last used ROMs if possible if let Some((_, path)) = self @@ -423,7 +434,14 @@ impl ModelSelectionDialog { .unwrap_or_else(|| self.selected_model.ram_size_default()), )) .show_ui(ui, |ui| { - for &sz in self.selected_model.ram_size_options() { + for &sz in self + .selected_model + .ram_size_options() + .iter() + .filter(|&&sz| { + !self.init_args.slim_adapter || sz <= SLIM_ADAPTER_MAX_RAM + }) + { ui.selectable_value( &mut self.init_args.ram_size, Some(sz), @@ -552,6 +570,13 @@ impl ModelSelectionDialog { if matches!(self.selected_model, MacModel::MacII | MacModel::MacIIFDHD) { ui.checkbox(&mut self.init_args.pmmu_enabled, "Enable 68851 PMMU"); } + if self.selected_model.has_slim() + && ui + .checkbox(&mut self.init_args.slim_adapter, "SLIM card adapter") + .changed() + { + self.clamp_ram_for_slim_adapter(); + } }); ui.separator(); @@ -704,6 +729,9 @@ impl ModelSelectionDialog { if !matches!(self.selected_model, MacModel::MacII | MacModel::MacIIFDHD) { self.init_args.pmmu_enabled = false; } + if !self.selected_model.has_slim() { + self.init_args.slim_adapter = false; + } self.result = Some(ModelSelectionResult { model: self.selected_model, @@ -765,6 +793,20 @@ impl ModelSelectionDialog { } } + /// Limits the RAM size to 5MB if the SLIM adapter is installed + fn clamp_ram_for_slim_adapter(&mut self) { + if !self.init_args.slim_adapter { + return; + } + let ram_size = self + .init_args + .ram_size + .unwrap_or_else(|| self.selected_model.ram_size_default()); + if ram_size > SLIM_ADAPTER_MAX_RAM { + self.init_args.ram_size = Some(SLIM_ADAPTER_MAX_RAM); + } + } + fn do_validate_roms(&mut self) { self.do_validate_display_rom(); self.do_validate_main_rom(); diff --git a/frontend_egui/src/emulator.rs b/frontend_egui/src/emulator.rs index 72312486..c540e200 100644 --- a/frontend_egui/src/emulator.rs +++ b/frontend_egui/src/emulator.rs @@ -18,7 +18,7 @@ use snow_core::cpu_m68k::regs::{Register, RegisterFile}; use snow_core::debuggable::DebuggableProperties; use snow_core::emulator::comm::{ Breakpoint, EmulatorCommand, EmulatorEvent, EmulatorSpeed, FddStatus, ScsiTargetStatus, - UserMessageType, + SlimSlotStatus, UserMessageType, }; use snow_core::emulator::comm::{EmulatorCommandSender, EmulatorEventReceiver, EmulatorStatus}; use snow_core::emulator::{Emulator, MouseMode}; @@ -106,6 +106,9 @@ pub struct EmulatorInitArgs { /// Enable PMMU (Macintosh II only) pub pmmu_enabled: bool, + /// Enable SLIM card adapter (Macintosh Portable only) + pub slim_adapter: bool, + /// Selected NuBus video card (if applicable) pub video_card: NubusDeviceKind, @@ -253,6 +256,7 @@ impl EmulatorState { args.ram_size, args.override_fdd_type, args.pmmu_enabled, + args.slim_adapter, shared_dir, )?; @@ -839,6 +843,31 @@ impl EmulatorState { .unwrap(); } + pub fn get_slim_status(&self) -> Option<&[Option]> { + let status = self.status.as_ref()?; + status.has_slim.then_some(&status.slim[..]) + } + + pub fn slim_insert(&self, slot: usize, path: &Path, write_protect: bool) { + let Some(ref sender) = self.cmdsender else { + return; + }; + sender + .send(EmulatorCommand::SlimInsert( + slot, + path.to_path_buf(), + write_protect, + )) + .unwrap(); + } + + pub fn slim_eject(&self, slot: usize) { + let Some(ref sender) = self.cmdsender else { + return; + }; + sender.send(EmulatorCommand::SlimEject(slot)).unwrap(); + } + /// Loads a SCSI HDD image from the specified path. pub fn scsi_attach_hdd(&self, id: usize, path: &Path) { let Some(ref sender) = self.cmdsender else { @@ -1026,6 +1055,11 @@ impl EmulatorState { self.status.as_ref().is_some_and(|s| s.has_pmmu) } + /// Returns true if the SLIM adapter is installed + pub fn has_slim(&self) -> bool { + self.status.as_ref().is_some_and(|s| s.has_slim) + } + /// Returns current disassembly listing pub fn get_disassembly(&self) -> &DisassemblyListing { &self.disasm_code diff --git a/frontend_egui/src/settings.rs b/frontend_egui/src/settings.rs index 36494a11..feacbc07 100644 --- a/frontend_egui/src/settings.rs +++ b/frontend_egui/src/settings.rs @@ -53,6 +53,7 @@ pub struct AppSettings { pub fd_cdrom_files: FileDialogStorage, pub fd_cdrom_folder: FileDialogStorage, pub fd_floppy: FileDialogStorage, + pub fd_slim: FileDialogStorage, pub fd_record: FileDialogStorage, pub fd_workspace: FileDialogStorage, pub fd_state: FileDialogStorage, @@ -96,6 +97,7 @@ impl Default for AppSettings { fd_cdrom_files: Default::default(), fd_cdrom_folder: Default::default(), fd_floppy: Default::default(), + fd_slim: Default::default(), fd_record: Default::default(), fd_workspace: Default::default(), fd_state: Default::default(), diff --git a/frontend_egui/src/workspace.rs b/frontend_egui/src/workspace.rs index 98584814..b91b155e 100644 --- a/frontend_egui/src/workspace.rs +++ b/frontend_egui/src/workspace.rs @@ -10,6 +10,7 @@ use crate::widgets::framebuffer::ScalingAlgorithm; use anyhow::{Context, Result}; use eframe::egui; use serde::{Deserialize, Deserializer, Serialize}; +use snow_core::emulator::comm::SlimSlotStatus; use snow_core::mac::MacModel; #[cfg(feature = "ethernet")] use snow_core::mac::scsi::ethernet::EthernetLinkType; @@ -144,6 +145,22 @@ impl Into for WorkspaceScsiTarget { } } +/// SLIM card inserted into a slot of the Macintosh Portable SLIM adapter +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WorkspaceSlimCard { + pub image: RelativePath, + pub write_protect: bool, +} + +impl From for WorkspaceSlimCard { + fn from(value: SlimSlotStatus) -> Self { + Self { + image: RelativePath::from_absolute(&value.image), + write_protect: value.write_protect, + } + } +} + /// Workspace variant of link mode for ethernet device #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -261,6 +278,9 @@ pub struct Workspace { /// Configured SCSI targets scsi_targets: [WorkspaceScsiTarget; 7], + /// Inserted SLIM cards + slim_cards: [Option; 2], + /// Window positions windows: HashMap, @@ -325,6 +345,7 @@ impl Default for Workspace { extension_rom_path: None, disks: Default::default(), scsi_targets: Default::default(), + slim_cards: Default::default(), windows: HashMap::new(), init_args: EmulatorInitArgs::default(), model: None, @@ -399,6 +420,9 @@ impl Workspace { | WorkspaceScsiTarget::Printer => (), } } + for c in result.slim_cards.iter_mut().flatten() { + c.image.after_deserialize(parent)?; + } for (i, d) in result.disks.iter_mut().enumerate() { if let Some(p) = d.as_mut() { p.after_deserialize(parent)?; @@ -464,6 +488,9 @@ impl Workspace { | WorkspaceScsiTarget::Printer => (), } } + for c in self.slim_cards.iter_mut().flatten() { + c.image.before_serialize(parent)?; + } for p in &mut self.floppy_images { p.before_serialize(parent)?; } @@ -480,6 +507,18 @@ impl Workspace { self.scsi_targets[id] = target.into().try_into().unwrap_or_default(); } + pub fn slim_cards(&self) -> [Option<(PathBuf, bool)>; 2] { + core::array::from_fn(|i| { + self.slim_cards[i] + .as_ref() + .map(|c| (c.image.get_absolute(), c.write_protect)) + }) + } + + pub fn set_slim_card(&mut self, slot: usize, card: Option) { + self.slim_cards[slot] = card.map(Into::into); + } + pub fn set_rom_path(&mut self, p: &Path) { self.rom_path = Some(RelativePath::from_absolute(p)); } From 3ad1357e635ba428bd52bb217d796dcd48342509 Mon Sep 17 00:00:00 2001 From: Reza Fouladian <9205977+rezafouladian@users.noreply.github.com> Date: Fri, 25 Sep 2026 07:59:44 -0700 Subject: [PATCH 2/4] Add drag and drop SLIM image support --- frontend_egui/src/app.rs | 19 ++++++++++++++++++- frontend_egui/src/emulator.rs | 12 ++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend_egui/src/app.rs b/frontend_egui/src/app.rs index 1bf7a8e3..ecac3710 100644 --- a/frontend_egui/src/app.rs +++ b/frontend_egui/src/app.rs @@ -39,7 +39,7 @@ use strum::IntoEnumIterator; use std::collections::VecDeque; use std::fs::File; -use std::io::{Seek, SeekFrom, Write}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::sync::{ Arc, @@ -3299,6 +3299,23 @@ impl SnowGui { ); } } + "slim" | "bin" => { + // Check for an EDisk header to avoid accidental file corruption + let mut hdr = [0u8; 144]; + if File::open(path) + .and_then(|mut f| f.read_exact(&mut hdr)) + .is_ok() + && &hdr[132..144] == b"EDisk Gary D" + { + if let Err(e) = self.emu.slim_insert_firstfree(path, false) { + self.toasts.add( + Toast::new() + .text(format!("Cannot load SLIM image: {}", e)) + .kind(ToastKind::Error), + ); + } + } + } _ => { self.toasts.add( Toast::new() diff --git a/frontend_egui/src/emulator.rs b/frontend_egui/src/emulator.rs index c540e200..8467d2bc 100644 --- a/frontend_egui/src/emulator.rs +++ b/frontend_egui/src/emulator.rs @@ -861,6 +861,18 @@ impl EmulatorState { .unwrap(); } + /// Inserts a SLIM card into the first available slot + pub fn slim_insert_firstfree(&self, path: &Path, write_protect: bool) -> Result { + let Some(slots) = self.get_slim_status() else { + return Err("No SLIM adapter is installed"); + }; + let Some(slot) = slots.iter().position(Option::is_none) else { + return Err("No free SLIM card slots"); + }; + self.slim_insert(slot, path, write_protect); + Ok(slot) + } + pub fn slim_eject(&self, slot: usize) { let Some(ref sender) = self.cmdsender else { return; From 0a91c6c6aaae1fc20c4a4fa1dbb5ae6354397ccd Mon Sep 17 00:00:00 2001 From: Reza Fouladian <9205977+rezafouladian@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:04:58 -0700 Subject: [PATCH 3/4] Add software gated write protect for SLIMs --- core/src/mac/portable/normandy.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/mac/portable/normandy.rs b/core/src/mac/portable/normandy.rs index d528e148..ab782dd7 100644 --- a/core/src/mac/portable/normandy.rs +++ b/core/src/mac/portable/normandy.rs @@ -314,12 +314,16 @@ impl BusMember
for Normandy { match addr { // SLIM card 1 space 0x50_0000..=0x6F_FFFF => { - self.slim_write(0, (addr - 0x50_0000) as usize, val); + if !self.slim1_protect.protect() { + self.slim_write(0, (addr - 0x50_0000) as usize, val); + } Some(()) } // SLIM card 2 space 0x70_0000..=0x8F_FFFF => { - self.slim_write(1, (addr - 0x70_0000) as usize, val); + if !self.slim2_protect.protect() { + self.slim_write(1, (addr - 0x70_0000) as usize, val); + } Some(()) } // SLIM adapter registers From e81a6f989aaa23219a13426f234ae8ee22d0e266 Mon Sep 17 00:00:00 2001 From: Reza Fouladian <9205977+rezafouladian@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:48:54 -0700 Subject: [PATCH 4/4] Portable/SLIM save state fixes Prevents a video related panic when restoring a save state on the Portable. Reinsert SLIM cards when restoring a save state. Code formatting fixes. --- core/src/mac/portable/bus.rs | 3 ++- core/src/mac/portable/normandy.rs | 33 +++++++++++++++++++++++-------- core/src/mac/portable/video.rs | 5 +++++ frontend_egui/src/app.rs | 13 ++++++------ frontend_egui/src/emulator.rs | 6 +++++- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/core/src/mac/portable/bus.rs b/core/src/mac/portable/bus.rs index b94cd568..ab7d485e 100644 --- a/core/src/mac/portable/bus.rs +++ b/core/src/mac/portable/bus.rs @@ -179,7 +179,8 @@ where } /// Reinstalls things that can't be serialized and does some updates upon deserialization - pub fn after_deserialize(&mut self, _renderer: TRenderer) { + pub fn after_deserialize(&mut self, renderer: TRenderer) { + self.video.after_deserialize(renderer).unwrap(); self.asc.after_deserialize(); self.normandy.after_deserialize(); diff --git a/core/src/mac/portable/normandy.rs b/core/src/mac/portable/normandy.rs index ab782dd7..0f0493fc 100644 --- a/core/src/mac/portable/normandy.rs +++ b/core/src/mac/portable/normandy.rs @@ -7,11 +7,12 @@ use crate::bus::{Address, BusMember}; use crate::debuggable::{Debuggable, DebuggableProperties}; use crate::emulator::comm::SlimSlotStatus; -use crate::mac::scsi::disk_image::DiskImage; +use crate::mac::scsi::disk_image::{DiskImage, FileDiskImage}; use crate::tickable::{Tickable, Ticks}; use anyhow::{Result, bail}; use proc_bitfield::bitfield; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; const IDLE_DTACK_DELAY: u8 = 64; const SLIM_DTACK_DELAY: u8 = 16; @@ -61,9 +62,12 @@ bitfield! { } } -#[derive(Default)] +#[derive(Default, Serialize, Deserialize)] struct SlimCard { + #[serde(skip)] image: Option>, + /// Path of the card image, used to reattach the image after loading a save state + path: Option, write_protect: bool, } @@ -85,7 +89,6 @@ pub struct Normandy { slim2_protect: SlimProtect, slim_rom: Vec, - #[serde(skip)] cards: [SlimCard; 2], pub dtack_counter: u8, @@ -113,12 +116,24 @@ impl Normandy { } } - /// Card images aren't serialized, so report both slots as empty after loading a state pub(crate) fn after_deserialize(&mut self) { - self.slim1_status.set_inserted(false); - self.slim1_status.set_readonly(false); - self.slim2_status.set_inserted(false); - self.slim2_status.set_readonly(false); + for slot in 0..self.cards.len() { + let card = &mut self.cards[slot]; + let Some(path) = &card.path else { continue }; + match FileDiskImage::open(path, !card.write_protect) { + Ok(image) => card.image = Some(Box::new(image)), + Err(e) => { + log::error!("Cannot reinsert SLIM card {}: {:?}", slot, e); + card.path = None; + let status = match slot { + 0 => &mut self.slim1_status, + _ => &mut self.slim2_status, + }; + status.set_inserted(false); + status.set_readonly(false); + } + } + } } pub(crate) fn slim_installed(&self) -> bool { @@ -162,6 +177,7 @@ impl Normandy { } } self.cards[slot] = SlimCard { + path: image.image_path().map(|p| p.to_path_buf()), image: Some(image), write_protect, }; @@ -173,6 +189,7 @@ impl Normandy { bail!("Invalid SLIM slot {}", slot); } self.cards[slot].image = None; + self.cards[slot].path = None; match slot { 0 => { self.slim1_eject.set_eject(false); diff --git a/core/src/mac/portable/video.rs b/core/src/mac/portable/video.rs index c1ab60a4..c20333d3 100644 --- a/core/src/mac/portable/video.rs +++ b/core/src/mac/portable/video.rs @@ -62,6 +62,11 @@ where Ok(()) } + pub(crate) fn after_deserialize(&mut self, renderer: T) -> Result<()> { + self.renderer = Some(renderer); + self.render() + } + pub(crate) fn blank(&mut self) -> Result<()> { self.framebuffer.fill(0xFF); self.render() diff --git a/frontend_egui/src/app.rs b/frontend_egui/src/app.rs index ecac3710..c31d2b50 100644 --- a/frontend_egui/src/app.rs +++ b/frontend_egui/src/app.rs @@ -3306,14 +3306,13 @@ impl SnowGui { .and_then(|mut f| f.read_exact(&mut hdr)) .is_ok() && &hdr[132..144] == b"EDisk Gary D" + && let Err(e) = self.emu.slim_insert_firstfree(path, false) { - if let Err(e) = self.emu.slim_insert_firstfree(path, false) { - self.toasts.add( - Toast::new() - .text(format!("Cannot load SLIM image: {}", e)) - .kind(ToastKind::Error), - ); - } + self.toasts.add( + Toast::new() + .text(format!("Cannot load SLIM image: {}", e)) + .kind(ToastKind::Error), + ); } } _ => { diff --git a/frontend_egui/src/emulator.rs b/frontend_egui/src/emulator.rs index 8467d2bc..40a04489 100644 --- a/frontend_egui/src/emulator.rs +++ b/frontend_egui/src/emulator.rs @@ -862,7 +862,11 @@ impl EmulatorState { } /// Inserts a SLIM card into the first available slot - pub fn slim_insert_firstfree(&self, path: &Path, write_protect: bool) -> Result { + pub fn slim_insert_firstfree( + &self, + path: &Path, + write_protect: bool, + ) -> Result { let Some(slots) = self.get_slim_status() else { return Err("No SLIM adapter is installed"); };