Bladeren bron

wizard: create classic Mac OS machine

Also add support for classicvirtio.

Resolves #6520
osy 2 weken geleden
bovenliggende
commit
203f5261dd

+ 2 - 0
Configuration/QEMUConstant.swift

@@ -411,6 +411,7 @@ extension QEMUArchitecture {
         case .avr: return false
         case .m68k: return false
         case .microblaze, .microblazeel: return false
+        case .ppc, .ppc64: return false
         case .rx: return false
         case .sparc, .sparc64: return false
         case .tricore: return false
@@ -429,6 +430,7 @@ extension QEMUArchitecture {
         switch self {
         case .s390x: return false
         case .sparc, .sparc64: return false
+        case .m68k: return false
         default: return true
         }
     }

+ 4 - 0
Configuration/QEMUConstantGenerated.swift

@@ -7594,10 +7594,12 @@ enum QEMUNetworkDevice_loongarch64: String, CaseIterable, QEMUNetworkDevice {
 
 enum QEMUNetworkDevice_m68k: String, CaseIterable, QEMUNetworkDevice {
     case virtio_net_device = "virtio-net-device"
+    case dp8393x = "dp8393x"
 
     var prettyValue: String {
         switch self {
         case .virtio_net_device: return "virtio-net-device"
+        case .dp8393x: return "SONIC DP8393x (Q800 only)"
         }
     }
 }
@@ -8774,10 +8776,12 @@ enum QEMUSoundDevice_loongarch64: String, CaseIterable, QEMUSoundDevice {
 
 enum QEMUSoundDevice_m68k: String, CaseIterable, QEMUSoundDevice {
     case virtio_sound_device = "virtio-sound-device"
+    case asc = "asc"
 
     var prettyValue: String {
         switch self {
         case .virtio_sound_device: return "virtio-sound-device"
+        case .asc: return "Apple Sound Chip (Q800 only)"
         }
     }
 }

+ 83 - 15
Configuration/UTMQemuConfiguration+Arguments.swift

@@ -136,6 +136,7 @@ import Virtualization // for getting network interfaces
         if isUsbUsed {
             usbArguments
         }
+        otherInputsArguments
         drivesArguments
         sharingArguments
         miscArguments
@@ -241,6 +242,10 @@ import Virtualization // for getting network interfaces
         }
     }
 
+    private func shouldSkipDisplay(_ display: UTMQemuConfigurationDisplay) -> Bool {
+        return display.hardware.rawValue == QEMUDisplayDevice_m68k.nubus_macfb.rawValue
+    }
+
     @QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
         if displays.isEmpty {
             f("-nographic")
@@ -253,12 +258,17 @@ import Virtualization // for getting network interfaces
             f()
         } else {
             for display in displays {
-                f("-device")
-                filterDisplayIfRemote(display.hardware)
-                if let vgaRamSize = displays[0].vgaRamMib {
-                    "vgamem_mb=\(vgaRamSize)"
+                if !shouldSkipDisplay(display) {
+                    f("-device")
+                    filterDisplayIfRemote(display.hardware)
+                    if let vgaRamSize = displays[0].vgaRamMib {
+                        "vgamem_mb=\(vgaRamSize)"
+                    }
+                    if display.hardware.rawValue.lowercased().contains("vga") && isClassicMacNewWorld {
+                        "edid=on"
+                    }
+                    f()
                 }
-                f()
             }
         }
     }
@@ -277,6 +287,14 @@ import Virtualization // for getting network interfaces
         qemu.spiceServerPort != nil
     }
 
+    private var isClassicMacM68K: Bool {
+        system.architecture == .m68k && system.target.rawValue == QEMUTarget_m68k.q800.rawValue
+    }
+
+    private var isClassicMacNewWorld: Bool {
+        [.ppc, .ppc64].contains(system.architecture) && system.target.rawValue == QEMUTarget_ppc.mac99.rawValue
+    }
+
     @QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
         for i in serials.indices {
             f("-chardev")
@@ -485,7 +503,12 @@ import Virtualization // for getting network interfaces
                 properties = properties.appendingDefaultPropertyName("gic-version", value: "3")
             }
         }
-        if target == "mac99" {
+        if isClassicMacM68K {
+            if sound.contains(where: { $0.hardware.rawValue == QEMUSoundDevice_m68k.asc.rawValue }) {
+                properties = properties.appendingDefaultPropertyName("audiodev", value: "audio0")
+            }
+        }
+        if isClassicMacNewWorld {
             properties = properties.appendingDefaultPropertyName("via", value: "pmu")
         }
         return properties
@@ -523,6 +546,25 @@ import Virtualization // for getting network interfaces
                 f()
             }
         }
+        if isClassicMacM68K {
+            let declrom = resourceURL.appendingPathComponent("m68k-declrom")
+            f("-device")
+            "nubus-virtio-mmio"
+            "romfile="
+            declrom
+            f()
+        }
+        if isClassicMacNewWorld {
+            let ndrvloader = resourceURL.appendingPathComponent("ppc-ndrvloader")
+            f("-device")
+            "loader"
+            "addr=0x4000000"
+            "file="
+            ndrvloader
+            f()
+            f("-prom-env")
+            f("boot-command=init-program go")
+        }
         f("-m")
         system.memorySize
         f()
@@ -572,7 +614,11 @@ import Virtualization // for getting network interfaces
         return false
         #endif
     }
-    
+
+    private func isInternalAudioDevice(_ device: any QEMUSoundDevice) -> Bool {
+        [QEMUSoundDevice_i386.pcspk.rawValue, QEMUSoundDevice_ppc.screamer.rawValue, QEMUSoundDevice_m68k.asc.rawValue].contains(device.rawValue)
+    }
+
     @QEMUArgumentBuilder private var soundArguments: [QEMUArgument] {
         if sound.isEmpty {
             f("-audio")
@@ -596,7 +642,7 @@ import Virtualization // for getting network interfaces
         "spice"
         f("id=audio0")
         // screamer has no extra device, pcspk is handled in machineProperties
-        for _sound in sound.filter({ $0.hardware.rawValue != "screamer" && $0.hardware.rawValue != "pcspk" }) {
+        for _sound in sound.filter({ !isInternalAudioDevice($0.hardware) }) {
             f("-device")
             _sound.hardware
             if _sound.hardware.rawValue.contains("hda") {
@@ -834,7 +880,7 @@ import Virtualization // for getting network interfaces
         }
         f()
     }
-    
+
     @QEMUArgumentBuilder private var usbArguments: [QEMUArgument] {
         if system.target.rawValue.hasPrefix("virt") {
             f("-device")
@@ -842,8 +888,10 @@ import Virtualization // for getting network interfaces
         } else {
             f("-usb")
         }
-        f("-device")
-        f("usb-tablet,bus=usb-bus.0")
+        if !isClassicMacNewWorld {
+            f("-device")
+            f("usb-tablet,bus=usb-bus.0")
+        }
         f("-device")
         f("usb-mouse,bus=usb-bus.0")
         f("-device")
@@ -881,7 +929,18 @@ import Virtualization // for getting network interfaces
         }
         #endif
     }
-    
+
+    @QEMUArgumentBuilder private var otherInputsArguments: [QEMUArgument] {
+        if isClassicMacNewWorld {
+            f("-device")
+            f("virtio-tablet-pci")
+        }
+        if isClassicMacM68K {
+            f("-device")
+            f("virtio-tablet-device")
+        }
+    }
+
     private func parseNetworkSubnet(from network: UTMQemuConfigurationNetwork) -> (start: String, end: String, mask: String)? {
         guard let net = network.vlanGuestAddress else {
             return nil
@@ -928,10 +987,13 @@ import Virtualization // for getting network interfaces
     
     @QEMUArgumentBuilder private var networkArguments: [QEMUArgument] {
         for i in networks.indices {
-            if isSparc {
+            if (isSparc && networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue) ||
+                (isClassicMacM68K && networks[i].hardware.rawValue == QEMUNetworkDevice_m68k.dp8393x.rawValue) {
                 f("-net")
                 "nic"
-                "model=lance"
+                if networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue {
+                    "model=lance"
+                }
                 "macaddr=\(networks[i].macAddress)"
                 "netdev=net\(i)"
                 f()
@@ -1076,11 +1138,17 @@ import Virtualization // for getting network interfaces
             f("-device")
             if system.architecture == .s390x {
                 "virtio-9p-ccw"
+            } else if system.architecture == .m68k {
+                "virtio-9p-device"
             } else {
                 "virtio-9p-pci"
             }
             "fsdev=virtfs0"
-            "mount_tag=share"
+            if isClassicMacM68K || isClassicMacNewWorld {
+                "mount_tag=share_1"
+            } else {
+                "mount_tag=share"
+            }
         }
     }
     

+ 2 - 0
Configuration/UTMQemuConfigurationDisplay.swift

@@ -86,6 +86,8 @@ extension UTMQemuConfigurationDisplay {
             hardware = QEMUDisplayDevice_x86_64.isa_vga
         } else if rawTarget.hasPrefix("virt-") || rawTarget == "virt" {
             hardware = QEMUDisplayDevice_aarch64.virtio_ramfb
+        } else if architecture == .m68k && rawTarget == QEMUTarget_m68k.q800.rawValue {
+            hardware = QEMUDisplayDevice_m68k.nubus_macfb
         } else {
             let cards = architecture.displayDeviceType.allRawValues
             if cards.contains("VGA") {

+ 1 - 1
Configuration/UTMQemuConfigurationDrive.swift

@@ -137,7 +137,7 @@ extension UTMQemuConfigurationDrive {
             } else {
                 return .virtio
             }
-        } else if architecture == .sparc || architecture == .sparc64 {
+        } else if architecture == .sparc || architecture == .sparc64 || architecture == .m68k {
             return .scsi
         } else {
             return .ide

+ 4 - 0
Configuration/UTMQemuConfigurationNetwork.swift

@@ -169,6 +169,10 @@ extension UTMQemuConfigurationNetwork {
             hardware = QEMUNetworkDevice_x86_64.ne2k_isa
         } else if rawTarget.hasPrefix("virt-") || rawTarget == "virt" {
             hardware = QEMUNetworkDevice_aarch64.virtio_net_pci
+        } else if [.ppc, .ppc64].contains(architecture) && rawTarget == QEMUTarget_ppc.mac99.rawValue {
+            hardware = QEMUNetworkDevice_ppc.sungem
+        } else if architecture == .m68k && rawTarget == QEMUTarget_m68k.q800.rawValue {
+            hardware = QEMUNetworkDevice_m68k.dp8393x
         } else {
             let cards = architecture.networkDeviceType.allRawValues
             if let first = cards.first {

+ 4 - 0
Configuration/UTMQemuConfigurationSharing.swift

@@ -73,6 +73,10 @@ extension UTMQemuConfigurationSharing {
         } else if (architecture == .arm || architecture == .aarch64) && (rawTarget.hasPrefix("virt-") || rawTarget == "virt") {
             directoryShareMode = .webdav
             hasClipboardSharing = true
+        } else if architecture == .m68k && rawTarget == QEMUTarget_m68k.q800.rawValue {
+            directoryShareMode = .virtfs
+        } else if [.ppc, .ppc64].contains(architecture) && rawTarget == QEMUTarget_ppc.mac99.rawValue {
+            directoryShareMode = .virtfs
         }
     }
 }

+ 2 - 0
Configuration/UTMQemuConfigurationSound.swift

@@ -53,6 +53,8 @@ extension UTMQemuConfigurationSound {
             hardware = QEMUSoundDevice_x86_64.intel_hda
         } else if rawTarget == "mac99" {
             hardware = QEMUSoundDevice_ppc.screamer
+        } else if architecture == .m68k && rawTarget == QEMUTarget_m68k.q800.rawValue {
+            hardware = QEMUSoundDevice_m68k.asc
         } else {
             let cards = architecture.soundDeviceType.allRawValues
             if let first = cards.first {

BIN
Icons/macos.png


+ 1 - 1
Platform/Shared/VMSettingsAddDeviceMenuView.swift

@@ -36,7 +36,7 @@ struct VMSettingsAddDeviceMenuView: View {
     }
     
     private var isAddDisplayEnabled: Bool {
-        if config.system.architecture == .sparc || config.system.architecture == .sparc64 {
+        if [.sparc, .sparc64, .m68k].contains(config.system.architecture) {
             return config.displays.count < 1
         } else {
             return !config.system.architecture.displayDeviceType.allRawValues.isEmpty

+ 114 - 22
Platform/Shared/VMWizardHardwareView.swift

@@ -20,8 +20,71 @@ import Virtualization
 #endif
 
 struct VMWizardHardwareView: View {
+    private enum ClassicMacSystem: CaseIterable, Identifiable {
+        case quadra800
+        //case powerMacG3Beige
+        case powerMacG4
+        //case powerMacG5
+
+        var id: Self { self }
+
+        var title: LocalizedStringKey {
+            switch self {
+            case .quadra800: "Macintosh Quadra 800 (M68K)"
+            //case .powerMacG3Beige: "Power Macintosh G3 (Beige)"
+            case .powerMacG4: "Power Macintosh G4 (PPC)"
+            //case .powerMacG5: "Power Macintosh G5 (PPC64)"
+            }
+        }
+
+        var architecture: QEMUArchitecture {
+            switch self {
+            case .quadra800: return .m68k
+            //case .powerMacG3Beige: return .ppc
+            case .powerMacG4: return .ppc
+            //case .powerMacG5: return .ppc64
+            }
+        }
+
+        var target: any QEMUTarget {
+            switch self {
+            case .quadra800: return QEMUTarget_m68k.q800
+            //case .powerMacG3Beige: return QEMUTarget_ppc.g3beige
+            case .powerMacG4: return QEMUTarget_ppc.mac99
+            //case .powerMacG5: return QEMUTarget_ppc.mac99
+            }
+        }
+
+        var minRam: Int {
+            switch self {
+            case .quadra800: return 8
+            //case .powerMacG3Beige: return 32
+            case .powerMacG4: return 64
+            //case .powerMacG5: return 64
+            }
+        }
+
+        var maxRam: Int {
+            switch self {
+            case .quadra800: return 1024
+            //case .powerMacG3Beige: return 2047
+            case .powerMacG4: return 2048
+            //case .powerMacG5: return 2048
+            }
+        }
+
+        var defaultRam: Int {
+            switch self {
+            case .quadra800: return 128
+            //case .powerMacG3Beige: return 512
+            case .powerMacG4: return 512
+            //case .powerMacG5: return 512
+            }
+        }
+    }
     @ObservedObject var wizardState: VMWizardState
-    
+    @State private var classicMacSystem: ClassicMacSystem = .powerMacG4
+
     var minCores: Int {
         #if canImport(Virtualization)
         VZVirtualMachineConfiguration.minimumAllowedCPUCount
@@ -56,7 +119,7 @@ struct VMWizardHardwareView: View {
     
     var body: some View {
         VMWizardContent("Hardware") {
-            if !wizardState.useVirtualization {
+            if !wizardState.useVirtualization && wizardState.operatingSystem != .ClassicMacOS {
                 Section {
                     VMConfigConstantPicker(selection: $wizardState.systemArchitecture)
                         .onChange(of: wizardState.systemArchitecture) { newValue in
@@ -72,39 +135,61 @@ struct VMWizardHardwareView: View {
                     Text("System")
                 }
 
+            } else if wizardState.operatingSystem == .ClassicMacOS {
+                Section {
+                    Picker("Machine", selection: $classicMacSystem) {
+                        ForEach(ClassicMacSystem.allCases) { system in
+                            Text(system.title).tag(system)
+                        }
+                    }.pickerStyle(.inline)
+                    .onChange(of: classicMacSystem) { newValue in
+                        wizardState.systemArchitecture = newValue.architecture
+                        wizardState.systemTarget = newValue.target
+                        wizardState.systemMemoryMib = newValue.defaultRam
+                        wizardState.systemCpuCount = 1
+                        wizardState.storageSizeGib = 2
+                    }
+                }
             }
             Section {
                 RAMSlider(systemMemory: $wizardState.systemMemoryMib) { _ in
-                    if wizardState.systemMemoryMib > maxMemoryMib {
-                        wizardState.systemMemoryMib = maxMemoryMib
+                    let validMax = wizardState.operatingSystem == .ClassicMacOS ? classicMacSystem.maxRam : maxMemoryMib
+                    if wizardState.systemMemoryMib > validMax {
+                        wizardState.systemMemoryMib = validMax
+                    }
+                    let validMin = wizardState.operatingSystem == .ClassicMacOS ? classicMacSystem.minRam : 0
+                    if wizardState.systemMemoryMib < validMin {
+                        wizardState.systemMemoryMib = validMin
                     }
                 }
             } header: {
                 Text("Memory")
             }
-            
-            Section {
-                HStack {
-                    Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
-                        Text("CPU Cores")
-                    }
-                    NumberTextField("", number: $wizardState.systemCpuCount, prompt: "Default", onEditingChanged: { _ in
-                        guard wizardState.systemCpuCount != 0  else {
-                            return
-                        }
-                        if wizardState.systemCpuCount < minCores {
-                            wizardState.systemCpuCount = minCores
-                        } else if wizardState.systemCpuCount > maxCores {
-                            wizardState.systemCpuCount = maxCores
+
+            if wizardState.operatingSystem != .ClassicMacOS {
+                Section {
+                    HStack {
+                        Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
+                            Text("CPU Cores")
                         }
-                    })
+                        NumberTextField("", number: $wizardState.systemCpuCount, prompt: "Default", onEditingChanged: { _ in
+                            guard wizardState.systemCpuCount != 0  else {
+                                return
+                            }
+                            if wizardState.systemCpuCount < minCores {
+                                wizardState.systemCpuCount = minCores
+                            } else if wizardState.systemCpuCount > maxCores {
+                                wizardState.systemCpuCount = maxCores
+                            }
+                        })
                         .frame(width: 80)
                         .multilineTextAlignment(.trailing)
+                    }
+                } header: {
+                    Text("CPU")
                 }
-            } header: {
-                Text("CPU")
             }
-            
+
             
             
             if !wizardState.useAppleVirtualization && wizardState.operatingSystem == .Linux {
@@ -135,6 +220,13 @@ struct VMWizardHardwareView: View {
             if wizardState.legacyHardware && wizardState.systemArchitecture == .x86_64 {
                 wizardState.systemTarget = QEMUTarget_x86_64.pc
             }
+            if wizardState.operatingSystem == .ClassicMacOS {
+                wizardState.systemArchitecture = classicMacSystem.architecture
+                wizardState.systemTarget = classicMacSystem.target
+                wizardState.systemMemoryMib = classicMacSystem.defaultRam
+                wizardState.systemCpuCount = 1
+                wizardState.storageSizeGib = 2
+            }
         }
     }
     

+ 109 - 0
Platform/Shared/VMWizardOSClassicMacView.swift

@@ -0,0 +1,109 @@
+//
+// Copyright © 2025 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct VMWizardOSClassicMacView: View {
+    private enum PpcVia: CaseIterable, Identifiable {
+        case pmu
+        case pmuAdb
+        case cuda
+        
+        var id: Self { self }
+        
+        var title: LocalizedStringKey {
+            switch self {
+            case .pmu: return "PMU"
+            case .pmuAdb: return "PMU-ADB"
+            case .cuda: return "CUDA"
+            }
+        }
+
+        var machineProperties: String {
+            switch self {
+            case .pmu: return "via=pmu"
+            case .pmuAdb: return "via=pmu-adb"
+            case .cuda: return "via=cuda"
+            }
+        }
+    }
+
+    private enum SelectImage {
+        case bios
+        case bootImage
+    }
+
+    @ObservedObject var wizardState: VMWizardState
+    @State private var isFileImporterPresented: Bool = false
+    @State private var ppcVia: PpcVia = .pmu
+    @State private var selectImage: SelectImage = .bootImage
+
+    var body: some View {
+        VMWizardContent("Classic Mac OS") {
+            DetailedSection("Boot ISO Image") {
+                FileBrowseField(url: $wizardState.bootImageURL, isFileImporterPresented: $isFileImporterPresented, hasClearButton: false) {
+                    selectImage = .bootImage
+                }
+            }
+            
+            if wizardState.systemTarget.rawValue == QEMUTarget_m68k.q800.rawValue {
+                DetailedSection("Quadra 800 ROM") {
+                    FileBrowseField(url: $wizardState.quadra800RomUrl, isFileImporterPresented: $isFileImporterPresented, hasClearButton: false) {
+                        selectImage = .bios
+                    }
+                }
+            }
+            
+            if wizardState.systemArchitecture == .ppc || wizardState.systemArchitecture == .ppc64 {
+                DetailedSection("Advanced Options") {
+                    Picker("PMU", selection: $ppcVia) {
+                        ForEach(PpcVia.allCases) { item in
+                            Text(item.title).tag(item)
+                        }
+                    }.pickerStyle(.inline)
+                    .help("Different versions of Mac OS require different VIA option.")
+                    .onChange(of: ppcVia) { newValue in
+                        wizardState.machineProperties = newValue.machineProperties
+                    }
+                    .onAppear {
+                        wizardState.machineProperties = ppcVia.machineProperties
+                    }
+                }
+            }
+            
+            if wizardState.isBusy {
+                Spinner(size: .large)
+            }
+        }
+        .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data]) { result in
+            wizardState.busyWorkAsync {
+                let url = try result.get()
+                await MainActor.run {
+                    switch selectImage {
+                    case .bios:
+                        wizardState.quadra800RomUrl = url
+                    case .bootImage:
+                        wizardState.bootImageURL = url
+                    }
+                }
+            }
+        }
+    }
+}
+
+#Preview {
+    VMWizardOSClassicMacView(wizardState: VMWizardState())
+}

+ 11 - 0
Platform/Shared/VMWizardOSView.swift

@@ -34,6 +34,17 @@ struct VMWizardOSView: View {
                     }
                 }
                 #endif
+                if !wizardState.useVirtualization {
+                    Button {
+                        wizardState.operatingSystem = .ClassicMacOS
+                        wizardState.useAppleVirtualization = false
+                        wizardState.isGuestToolsInstallRequested = false
+                        wizardState.legacyHardware = true
+                        wizardState.next()
+                    } label: {
+                        OperatingSystem(imageName: "macos", name: "Classic Mac OS")
+                    }
+                }
                 Button {
                     wizardState.operatingSystem = .Windows
                     wizardState.useAppleVirtualization = false

+ 88 - 15
Platform/Shared/VMWizardState.swift

@@ -30,6 +30,7 @@ enum VMWizardPage: Int, Identifiable {
     case macOSBoot
     case linuxBoot
     case windowsBoot
+    case classicMacOSBoot
     case otherBoot
     case hardware
     case drives
@@ -37,15 +38,34 @@ enum VMWizardPage: Int, Identifiable {
     case summary
 }
 
-enum VMWizardOS: String, Identifiable {
-    var id: String {
-        return self.rawValue
-    }
-    
+enum VMWizardOS: Identifiable {
+    var id: Self { self }
+
     case Other
     case macOS
     case Linux
     case Windows
+    case ClassicMacOS
+
+    var name: LocalizedStringKey {
+        switch self {
+        case .Other: return "Other"
+        case .macOS: return "macOS"
+        case .Linux: return "Linux"
+        case .Windows: return "Windows"
+        case .ClassicMacOS: return "Mac OS"
+        }
+    }
+
+    var defaultIconName: String? {
+        switch self {
+        case .Other: return nil
+        case .macOS: return "mac"
+        case .Linux: return "linux"
+        case .Windows: return "windows"
+        case .ClassicMacOS: return "macos"
+        }
+    }
 }
 
 enum VMBootDevice: Int, Identifiable {
@@ -131,6 +151,7 @@ struct AlertMessage: Identifiable {
     @Published var linuxBootArguments: String = ""
     @Published var linuxHasRosetta: Bool = false
     @Published var isWindows10OrHigher: Bool = true
+    @Published var quadra800RomUrl: URL?
     @Published var systemArchitecture: QEMUArchitecture = .x86_64
     @Published var systemTarget: any QEMUTarget = QEMUTarget_x86_64.default
     #if os(macOS)
@@ -148,7 +169,8 @@ struct AlertMessage: Identifiable {
     @Published var name: String?
     @Published var isOpenSettingsAfterCreation: Bool = false
     @Published var useNvmeAsDiskInterface = false
-    
+    @Published var machineProperties: String?
+
     /// SwiftUI BUG: on macOS 12, when VoiceOver is enabled and isBusy changes the disable state of a button being clicked, 
     var isNeverDisabledWorkaround: Bool {
         #if os(macOS)
@@ -217,6 +239,8 @@ struct AlertMessage: Identifiable {
                 nextPage = .linuxBoot
             case .Windows:
                 nextPage = .windowsBoot
+            case .ClassicMacOS:
+                nextPage = .hardware
             }
         case .otherBoot:
             guard bootDevice == .none || bootImageURL != nil else {
@@ -271,6 +295,19 @@ struct AlertMessage: Identifiable {
                     }
                 }
             }
+            if operatingSystem == .ClassicMacOS {
+                nextPage = .classicMacOSBoot
+            }
+        case .classicMacOSBoot:
+            guard bootImageURL != nil else {
+                alertMessage = AlertMessage(NSLocalizedString("Please select a boot image.", comment: "VMWizardState"))
+                return
+            }
+            guard systemTarget.rawValue != QEMUTarget_m68k.q800.rawValue || quadra800RomUrl != nil else {
+                alertMessage = AlertMessage(NSLocalizedString("Please select a ROM file.", comment: "VMWizardState"))
+                return
+            }
+            nextPage = .drives
         case .drives:
             guard storageSizeGib > 0 else {
                 alertMessage = AlertMessage(NSLocalizedString("Invalid drive size specified.", comment: "VMWizardState"))
@@ -316,11 +353,13 @@ struct AlertMessage: Identifiable {
             config.drives.append(UTMAppleConfigurationDrive(existingURL: bootImageURL, isExternal: true))
         }
         var isSkipDiskCreate = false
+        if let iconName = operatingSystem.defaultIconName {
+            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: iconName)
+        }
         switch operatingSystem {
-        case .Other:
+        case .Other, .ClassicMacOS, .Windows:
             break
         case .macOS:
-            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "mac")
             #if os(macOS) && arch(arm64)
             if #available(macOS 12, *) {
                 config.system.boot = try! UTMAppleConfigurationBoot(for: .macOS)
@@ -329,7 +368,6 @@ struct AlertMessage: Identifiable {
             }
             #endif
         case .Linux:
-            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "linux")
             #if os(macOS)
             if bootDevice == .kernel {
                 var bootloader = try UTMAppleConfigurationBoot(for: .linux, linuxKernelURL: linuxKernelURL!)
@@ -346,8 +384,6 @@ struct AlertMessage: Identifiable {
             config.system.genericPlatform = UTMAppleConfigurationGenericPlatform()
             config.virtualization.hasRosetta = linuxHasRosetta
             #endif
-        case .Windows:
-            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "windows")
         }
         if !isSkipDiskCreate {
             var newDisk = UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib)
@@ -422,6 +458,8 @@ struct AlertMessage: Identifiable {
     #endif
     
     private func generateQemuConfig() throws -> UTMQemuConfiguration {
+        let isClassicMacM68K = systemArchitecture == .m68k && systemTarget.rawValue == QEMUTarget_m68k.q800.rawValue
+        let isClassicMacPPC = [.ppc, .ppc64].contains(systemArchitecture) && systemTarget.rawValue == QEMUTarget_ppc.mac99.rawValue
         let config = UTMQemuConfiguration()
         config.information.name = name!
         config.system.architecture = systemArchitecture
@@ -475,16 +513,23 @@ struct AlertMessage: Identifiable {
             } else if bootDevice == .drive {
                 bootDrive.interface = mainDriveInterface
             }
+            if isClassicMacM68K {
+                //bootDrive.interfaceLocation = [3, 0]
+            } else if isClassicMacPPC {
+                //bootDrive.interfaceLocation = [0, 1]
+            }
             bootDrive.imageURL = bootImageURL
             config.drives.append(bootDrive)
         }
+        if let iconName = operatingSystem.defaultIconName {
+            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: iconName)
+        }
         switch operatingSystem {
         case .Other:
             break
         case .macOS:
             throw NSLocalizedString("macOS is not supported with QEMU.", comment: "VMWizardState")
         case .Linux:
-            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "linux")
             if bootDevice == .kernel {
                 var kernel = UTMQemuConfigurationDrive()
                 kernel.imageURL = linuxKernelURL
@@ -511,15 +556,41 @@ struct AlertMessage: Identifiable {
                 }
             }
         case .Windows:
-            config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "windows")
             config.qemu.hasRTCLocalTime = true
+        case .ClassicMacOS:
+            if systemArchitecture == .ppc || systemArchitecture == .ppc64 {
+                config.qemu.machinePropertyOverride = machineProperties
+            }
+            if systemArchitecture == .m68k {
+                var pramDrive = UTMQemuConfigurationDrive()
+                pramDrive.sizeMib = 1
+                pramDrive.imageType = .disk
+                pramDrive.interface = .mtd
+                config.drives.append(pramDrive)
+                if let quadra800RomUrl = quadra800RomUrl {
+                    var bios = UTMQemuConfigurationDrive()
+                    bios.imageURL = quadra800RomUrl
+                    bios.imageType = .bios
+                    bios.isRawImage = true
+                    config.drives.append(bios)
+                }
+            }
         }
         if bootDevice != .drive {
             var diskImage = UTMQemuConfigurationDrive()
             diskImage.sizeMib = storageSizeGib * bytesInGib / bytesInMib
             diskImage.imageType = .disk
             diskImage.interface = mainDriveInterface
-            config.drives.append(diskImage)
+            if isClassicMacM68K {
+                //diskImage.interfaceLocation = [0, 0]
+            } else if isClassicMacPPC {
+                //diskImage.interfaceLocation = [0, 0]
+            }
+            if isClassicMacPPC || isClassicMacM68K {
+                config.drives.insert(diskImage, at: 0)
+            } else {
+                config.drives.append(diskImage)
+            }
             if operatingSystem == .Windows && isGuestToolsInstallRequested {
                 let toolsDiskDrive = UTMQemuConfigurationDrive(forArchitecture: systemArchitecture, target: systemTarget, isExternal: true)
                 config.drives.append(toolsDiskDrive)
@@ -527,7 +598,9 @@ struct AlertMessage: Identifiable {
         }
         if legacyHardware {
             config.qemu.hasUefiBoot = false
-            config.input.usbBusSupport = .usb2_0
+            if systemArchitecture.hasUsbSupport && systemTarget.hasUsbSupport {
+                config.input.usbBusSupport = .usb2_0
+            }
         }
         return config
     }

+ 2 - 2
Platform/Shared/VMWizardSummaryView.swift

@@ -91,7 +91,7 @@ struct VMWizardSummaryView: View {
                 if os == .Other {
                     wizardState.name = data.newDefaultVMName()
                 } else {
-                    wizardState.name = data.newDefaultVMName(base: os.rawValue)
+                    wizardState.name = data.newDefaultVMName(base: os.name.localizedString)
                 }
             }
             if #available(iOS 15, macOS 12, *) {
@@ -136,7 +136,7 @@ struct VMWizardSummaryView: View {
     
     var boot: some View {
         Group {
-            TextField("Operating System", text: .constant(NSLocalizedString(wizardState.operatingSystem.rawValue, comment: "VMWizardSummaryView")))
+            TextField("Operating System", text: .constant(wizardState.operatingSystem.name.localizedString))
             if let bootImageURL = wizardState.bootImageURL {
                 TextField("Boot Image", text: .constant(bootImageURL.path))
             }

+ 3 - 0
Platform/iOS/VMWizardView.swift

@@ -103,6 +103,7 @@ fileprivate struct WizardWrapper: View {
             NavigationLink(destination: WizardWrapper(page: .operatingSystem, wizardState: wizardState, onDismiss: onDismiss), tag: .operatingSystem, selection: $nextPage) {}
             NavigationLink(destination: WizardWrapper(page: .linuxBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .linuxBoot, selection: $nextPage) {}
             NavigationLink(destination: WizardWrapper(page: .windowsBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .windowsBoot, selection: $nextPage) {}
+            NavigationLink(destination: WizardWrapper(page: .classicMacOSBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .classicMacOSBoot, selection: $nextPage) {}
             NavigationLink(destination: WizardWrapper(page: .otherBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .otherBoot, selection: $nextPage) {}
             NavigationLink(destination: WizardWrapper(page: .hardware, wizardState: wizardState, onDismiss: onDismiss), tag: .hardware, selection: $nextPage) {}
             NavigationLink(destination: WizardWrapper(page: .drives, wizardState: wizardState, onDismiss: onDismiss), tag: .drives, selection: $nextPage) {}
@@ -181,6 +182,8 @@ fileprivate struct WizardViewWrapper: View {
             VMWizardOSWindowsView(wizardState: wizardState)
         case .otherBoot:
             VMWizardOSOtherView(wizardState: wizardState)
+        case .classicMacOSBoot:
+            VMWizardOSClassicMacView(wizardState: wizardState)
         case .hardware:
             VMWizardHardwareView(wizardState: wizardState)
         case .drives:

+ 3 - 0
Platform/macOS/VMWizardView.swift

@@ -60,6 +60,9 @@ struct VMWizardView: View {
             case .windowsBoot:
                 VMWizardOSWindowsView(wizardState: wizardState)
                     .transition(wizardState.slide)
+            case .classicMacOSBoot:
+                VMWizardOSClassicMacView(wizardState: wizardState)
+                    .transition(wizardState.slide)
             case .hardware:
                 VMWizardHardwareView(wizardState: wizardState)
                     .transition(wizardState.slide)

+ 10 - 0
UTM.xcodeproj/project.pbxproj

@@ -648,6 +648,10 @@
 		CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
 		CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
 		CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
+		CE68E5482E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */; };
+		CE68E5492E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */; };
+		CE68E54A2E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */; };
+		CE68E54B2E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */; };
 		CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
 		CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
 		CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
@@ -1974,6 +1978,7 @@
 		CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayWindowController.swift; sourceTree = "<group>"; };
 		CE66450C2269313200B0849A /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
 		CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMKeyboardShortcutsView.swift; sourceTree = "<group>"; };
+		CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardOSClassicMacView.swift; sourceTree = "<group>"; };
 		CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
 		CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = "<group>"; };
@@ -3048,6 +3053,7 @@
 				84C2E8642AA429E800B17308 /* VMWizardContent.swift */,
 				CEBE820226A4C1B5007AAB12 /* VMWizardDrivesView.swift */,
 				CEF0307326A2B40B00667B63 /* VMWizardHardwareView.swift */,
+				CE68E5472E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift */,
 				CEF0305726A2AFDE00667B63 /* VMWizardOSLinuxView.swift */,
 				CEF0305826A2AFDE00667B63 /* VMWizardOSMacView.swift */,
 				CEF0305426A2AFDD00667B63 /* VMWizardOSOtherView.swift */,
@@ -3652,6 +3658,7 @@
 				843BF83C2845494C0029D60D /* UTMQemuConfigurationSerial.swift in Sources */,
 				CEF0305126A2AFBF00667B63 /* Spinner.swift in Sources */,
 				CE88A1552E247CCE00EAA28E /* UTMIntent.swift in Sources */,
+				CE68E5492E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */,
 				CE88A09F2E1DDB4200EAA28E /* UTMASIFImage.m in Sources */,
 				843BF840284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */,
 				CE611BE729F50CAD001817BC /* UTMReleaseHelper.swift in Sources */,
@@ -3858,6 +3865,7 @@
 				CE25125329C80A18000790AB /* UTMScriptingCloneCommand.swift in Sources */,
 				CE88A1682E24E4C000EAA28E /* VMKeyboardMap.m in Sources */,
 				CE2D956A24AD4F990059923A /* VMPlaceholderView.swift in Sources */,
+				CE68E54B2E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */,
 				CEF0306626A2AFDF00667B63 /* VMWizardOSLinuxView.swift in Sources */,
 				CEE7E938287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */,
 				CEFE98E129485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift in Sources */,
@@ -4078,6 +4086,7 @@
 				8401865F2887B1620050AC51 /* VMDisplayTerminalViewController.swift in Sources */,
 				CEA45E8F263519B5002FA97D /* VMContextMenuModifier.swift in Sources */,
 				85EC516527CC8D0F004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */,
+				CE68E54A2E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */,
 				CE88A1612E24B2B400EAA28E /* UTMInputIntent.swift in Sources */,
 				03FA9C732B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */,
 				CEA45E91263519B5002FA97D /* VMDisplayMetalViewController+Pencil.m in Sources */,
@@ -4243,6 +4252,7 @@
 				CEF7F5CD2AEEDCC400E34952 /* UTMConfigurationTerminal.swift in Sources */,
 				CEF7F5CE2AEEDCC400E34952 /* VMWindowView.swift in Sources */,
 				CEF7F5CF2AEEDCC400E34952 /* UTMPendingVMView.swift in Sources */,
+				CE68E5482E3C3E0A006B3645 /* VMWizardOSClassicMacView.swift in Sources */,
 				CEE8B4C32B71E2BA0035AE86 /* UTMLoggingSwift.swift in Sources */,
 				CEF7F5D02AEEDCC400E34952 /* UTMSpiceIO.m in Sources */,
 				CEF7F5D12AEEDCC400E34952 /* UTMUnavailableVMView.swift in Sources */,

BIN
patches/data/qemu-10.0.2-utm/pc-bios/m68k-declrom


BIN
patches/data/qemu-10.0.2-utm/pc-bios/ppc-ndrvloader


+ 27 - 0
patches/qemu-10.0.2-utm.patch

@@ -273,3 +273,30 @@ index 0e8c4c1c67..e07f60357f 100644
 -- 
 2.41.0
 
+From 344a5a3cbe3df0c373743969493afe7d1c4fb4d6 Mon Sep 17 00:00:00 2001
+From: osy <osy@turing.llc>
+Date: Sat, 2 Aug 2025 19:22:04 -0700
+Subject: [PATCH] pc-bios: add classicvirtio drivers for m68k/ppc
+
+---
+ pc-bios/m68k-declrom   | Bin 0 -> 106496 bytes
+ pc-bios/meson.build    |   2 ++
+ pc-bios/ppc-ndrvloader | Bin 0 -> 191172 bytes
+ 3 files changed, 2 insertions(+)
+ create mode 100755 pc-bios/m68k-declrom
+ create mode 100644 pc-bios/ppc-ndrvloader
+
+diff --git a/pc-bios/meson.build b/pc-bios/meson.build
+index 9fb9659c45..63e10cc6df 100644
+--- a/pc-bios/meson.build
++++ b/pc-bios/meson.build
+@@ -85,6 +85,8 @@ blobs = [
+   'npcm8xx_bootrom.bin',
+   'vof.bin',
+   'vof-nvram.bin',
++  'm68k-declrom',
++  'ppc-ndrvloader',
+ ]
+ 
+ dtc = find_program('dtc', required: false)
+

+ 11 - 1
scripts/const-gen.py

@@ -56,10 +56,12 @@ DEFAULTS = {
 }
 
 AUDIO_SCREAMER = Device('screamer', 'macio', '', 'Screamer (Mac99 only)')
-AUDIO_PCSPK = Device('pcspk', 'macio', '', 'PC Speaker')
+AUDIO_PCSPK = Device('pcspk', 'none', '', 'PC Speaker')
+AUDIO_ASC = Device('asc', 'none', '', 'Apple Sound Chip (Q800 only)')
 DISPLAY_TCX = Device('tcx', 'none', '', 'Sun TCX')
 DISPLAY_CG3 = Device('cg3', 'none', '', 'Sun cgthree')
 NETWORK_LANCE = Device('lance', 'none', '', 'Lance (Am7990)')
+NETWORK_DP8393X = Device('dp8393x', 'none', '', 'SONIC DP8393x (Q800 only)')
 
 ADD_DEVICES = {
     "ppc": {
@@ -91,6 +93,14 @@ ADD_DEVICES = {
             AUDIO_PCSPK
         ])
     },
+    "m68k": {
+        "Sound devices": set([
+            AUDIO_ASC
+        ]),
+        "Network devices": set([
+            NETWORK_DP8393X
+        ])
+    }
 }
 
 HEADER = '''//