diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 40fa78f3f..08c8c206a 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -265,11 +265,13 @@ extension UTMAppleConfiguration { } if !ignoringDrives { vzconfig.storageDevices = try drives.compactMap { drive in - guard let attachment = try drive.vzDiskImage() else { + guard let attachment = try drive.vzDiskImage(useFsWorkAround: system.boot.operatingSystem == .linux) else { return nil } if #available(macOS 13, *), drive.isExternal { return VZUSBMassStorageDeviceConfiguration(attachment: attachment) + } else if #available(macOS 14, *), drive.isNvme, system.boot.operatingSystem == .linux { + return VZNVMExpressControllerDeviceConfiguration(attachment: attachment) } else { return VZVirtioBlockDeviceConfiguration(attachment: attachment) } diff --git a/Configuration/UTMAppleConfigurationDrive.swift b/Configuration/UTMAppleConfigurationDrive.swift index ef53dbbf6..e14488c35 100644 --- a/Configuration/UTMAppleConfigurationDrive.swift +++ b/Configuration/UTMAppleConfigurationDrive.swift @@ -25,6 +25,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { var sizeMib: Int = 0 var isReadOnly: Bool var isExternal: Bool + var isNvme: Bool var imageURL: URL? var imageName: String? @@ -36,6 +37,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { private enum CodingKeys: String, CodingKey { case isReadOnly = "ReadOnly" + case isNvme = "Nvme" case imageName = "ImageName" case bookmark = "Bookmark" // legacy only case identifier = "Identifier" @@ -55,12 +57,14 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { sizeMib = newSize isReadOnly = false isExternal = false + isNvme = false } - init(existingURL url: URL?, isExternal: Bool = false) { + init(existingURL url: URL?, isExternal: Bool = false, isNvme: Bool = false) { self.imageURL = url self.isReadOnly = isExternal self.isExternal = isExternal + self.isNvme = isNvme } init(from decoder: Decoder) throws { @@ -83,6 +87,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { isExternal = true } isReadOnly = try container.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? isExternal + isNvme = try container.decodeIfPresent(Bool.self, forKey: .isNvme) ?? false id = try container.decode(String.self, forKey: .identifier) } @@ -92,12 +97,18 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { try container.encodeIfPresent(imageName, forKey: .imageName) } try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encode(isNvme, forKey: .isNvme) try container.encode(id, forKey: .identifier) } - func vzDiskImage() throws -> VZDiskImageStorageDeviceAttachment? { + func vzDiskImage(useFsWorkAround: Bool = false) throws -> VZDiskImageStorageDeviceAttachment? { if let imageURL = imageURL { - return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + // Use cached caching mode for virtio drive to prevent fs corruption on linux when possible + if #available(macOS 12.0, *), !isNvme, useFsWorkAround { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly, cachingMode: .cached, synchronizationMode: .full) + } else { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + } } else { return nil } @@ -107,6 +118,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { imageName?.hash(into: &hasher) sizeMib.hash(into: &hasher) isReadOnly.hash(into: &hasher) + isNvme.hash(into: &hasher) isExternal.hash(into: &hasher) id.hash(into: &hasher) } @@ -127,6 +139,7 @@ extension UTMAppleConfigurationDrive { sizeMib = oldDrive.sizeMib isReadOnly = oldDrive.isReadOnly isExternal = oldDrive.isExternal + isNvme = false imageURL = oldDrive.imageURL } } diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index 2c3711033..48d9dbfc2 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/Platform/Shared/VMWizardState.swift @@ -135,6 +135,7 @@ enum VMBootDevice: Int, Identifiable { @Published var sharingReadOnly: Bool = false @Published var name: String? @Published var isOpenSettingsAfterCreation: Bool = false + @Published var useNvmeAsDiskInterface = false /// SwiftUI BUG: on macOS 12, when VoiceOver is enabled and isBusy changes the disable state of a button being clicked, var isNeverDisabledWorkaround: Bool { @@ -342,7 +343,11 @@ enum VMBootDevice: Int, Identifiable { } } if !isSkipDiskCreate { - config.drives.append(UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib)) + var newDisk = UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib) + if #available(macOS 14, *), useNvmeAsDiskInterface { + newDisk.isNvme = true + } + config.drives.append(newDisk) } if #available(macOS 12, *), let sharingDirectoryURL = sharingDirectoryURL { config.sharedDirectories = [UTMAppleConfigurationSharedDirectory(directoryURL: sharingDirectoryURL, isReadOnly: sharingReadOnly)] diff --git a/Platform/ja.lproj/Localizable.strings b/Platform/ja.lproj/Localizable.strings index b80d8d7ab..c88b81071 100644 --- a/Platform/ja.lproj/Localizable.strings +++ b/Platform/ja.lproj/Localizable.strings @@ -1141,3 +1141,9 @@ // UTMQemuMonitor.m "Guest panic" = "ゲストがパニック状態に陥りました"; + +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "NVMe を使用します"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "チェックされている場合、ディスクインターフェースとして virtio の代わりに NVMe を使用します。macOS 14+ では Linux ゲストのみ利用可能です。このインターフェースは遅いですが、ファイルシステムエラーが発生する可能性が低いです。"; + diff --git a/Platform/macOS/VMConfigAppleDriveCreateView.swift b/Platform/macOS/VMConfigAppleDriveCreateView.swift index 5d5244c74..74806d66b 100644 --- a/Platform/macOS/VMConfigAppleDriveCreateView.swift +++ b/Platform/macOS/VMConfigAppleDriveCreateView.swift @@ -33,11 +33,17 @@ struct VMConfigAppleDriveCreateView: View { if newValue { config.sizeMib = 0 config.isReadOnly = true + config.isNvme = false } else { config.sizeMib = 10240 config.isReadOnly = false } } + if #available(macOS 14, *), !config.isExternal { + Toggle(isOn: $config.isNvme.animation(), label: { + Text("Use NVMe Interface") + }).help("If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors.") + } if !config.isExternal { SizeTextField($config.sizeMib) } diff --git a/Platform/macOS/VMConfigAppleDriveDetailsView.swift b/Platform/macOS/VMConfigAppleDriveDetailsView.swift index b88cd0415..d96cfbbe1 100644 --- a/Platform/macOS/VMConfigAppleDriveDetailsView.swift +++ b/Platform/macOS/VMConfigAppleDriveDetailsView.swift @@ -28,6 +28,12 @@ struct VMConfigAppleDriveDetailsView: View { TextField("Name", text: .constant(config.imageURL?.lastPathComponent ?? NSLocalizedString("(New Drive)", comment: "VMConfigAppleDriveDetailsView"))) .disabled(true) Toggle("Read Only?", isOn: $config.isReadOnly) + if #available(macOS 14, *), !config.isExternal { + Toggle(isOn: $config.isNvme, + label: { + Text("Use NVMe Interface") + }).help("If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors.") + } if #unavailable(macOS 12) { Button { requestDriveDelete = config diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings index 6dcfee673..330e6fc9e 100644 --- a/Platform/zh-HK.lproj/Localizable.strings +++ b/Platform/zh-HK.lproj/Localizable.strings @@ -2297,3 +2297,8 @@ /* No comment provided by engineer. */ "Zoom" = "縮放"; + +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁碟介面"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果勾選,將使用 NVMe 而非 virtio 作為磁碟介面,僅在 macOS 14+ 中適用於 Linux 客戶機器。這個介面速度較慢,但較不容易遇到檔案系統錯誤。"; diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index b51e4e13a..81ccee3d4 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/Platform/zh-Hans.lproj/Localizable.strings @@ -2297,3 +2297,8 @@ /* No comment provided by engineer. */ "Zoom" = "缩放"; + +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁盘接口"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果选中,使用 NVMe 而不是 virtio 作为磁盘接口,仅适用于 macOS 14+ 上的 Linux 客户机。此接口速度较慢,但不太容易遇到文件系统错误。"; diff --git a/Platform/zh-Hant.lproj/Localizable.strings b/Platform/zh-Hant.lproj/Localizable.strings index ee2dd422b..8070b72bd 100644 --- a/Platform/zh-Hant.lproj/Localizable.strings +++ b/Platform/zh-Hant.lproj/Localizable.strings @@ -2002,3 +2002,8 @@ /* No comment provided by engineer. */ "Zoom" = "縮放"; +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁碟介面"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果選取,將使用 NVMe 而非 virtio 作為磁碟介面,僅在 macOS 14+ 中適用於 Linux 客戶機器。這個介面速度較慢,但較不容易遇到檔案系統錯誤。"; +