VMConfigSystemView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. //
  2. // Copyright © 2020 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import SwiftUI
  17. private let bytesInMib: UInt64 = 1024 * 1024
  18. private let minMemoryMib = 32
  19. private let baseUsageMib = 128
  20. private let warningThreshold = 0.9
  21. struct VMConfigSystemView: View {
  22. @Binding var config: UTMQemuConfigurationSystem
  23. @Binding var isResetConfig: Bool
  24. @State private var warningMessage: WarningMessage? = nil
  25. @State private var architecture: QEMUArchitecture = .x86_64
  26. @State private var target: any QEMUTarget = QEMUTarget_x86_64.pc
  27. var body: some View {
  28. VStack {
  29. Form {
  30. HardwareOptions(config: $config, architecture: $architecture, target: $target, warningMessage: $warningMessage)
  31. RAMSlider(systemMemory: $config.memorySize, onValidate: validateMemorySize)
  32. Section(header: Text("CPU")) {
  33. VMConfigConstantPicker(selection: $config.cpu, type: config.architecture.cpuType)
  34. }
  35. CPUFlagsOptions(title: "Force Enable CPU Flags", config: $config, flags: $config.cpuFlagsAdd)
  36. .help("If checked, the CPU flag will be enabled. Otherwise, the default value will be used.")
  37. CPUFlagsOptions(title: "Force Disable CPU Flags", config: $config, flags: $config.cpuFlagsRemove)
  38. .help("If checked, the CPU flag will be disabled. Otherwise, the default value will be used.")
  39. DetailedSection("CPU Cores", description: "Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation.") {
  40. HStack {
  41. NumberTextField("", number: $config.cpuCount, prompt: "Default", onEditingChanged: validateCpuCount)
  42. .multilineTextAlignment(.trailing)
  43. Text("Cores")
  44. }
  45. Toggle(isOn: $config.isForceMulticore, label: {
  46. Text("Force Multicore")
  47. })
  48. }
  49. DetailedSection("JIT Cache", description: "Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!") {
  50. HStack {
  51. NumberTextField("", number: $config.jitCacheSize, prompt: "Default", onEditingChanged: validateMemorySize)
  52. .multilineTextAlignment(.trailing)
  53. Text("MiB")
  54. }
  55. }
  56. }
  57. }.alert(item: $warningMessage) { warning in
  58. switch warning {
  59. case .overallocatedRam(_, _):
  60. return Alert(title: Text(warning.localizedWarningTitle), message: Text(warning.localizedWarningMessage))
  61. case .resetSystem:
  62. return Alert(title: Text(warning.localizedWarningTitle), message: Text(warning.localizedWarningMessage), primaryButton: .destructive(Text("Reset"), action: {
  63. config.architecture = architecture
  64. if !architecture.targetType.allRawValues.contains(target.rawValue) {
  65. target = architecture.targetType.default
  66. }
  67. config.target = target
  68. isResetConfig = true
  69. }), secondaryButton: .cancel(Text("Cancel"), action: {
  70. architecture = config.architecture
  71. target = config.target
  72. }))
  73. }
  74. }.disableAutocorrection(true)
  75. }
  76. func validateMemorySize(editing: Bool) {
  77. guard !editing else {
  78. return
  79. }
  80. let memorySizeMib = config.memorySize
  81. guard memorySizeMib >= minMemoryMib else {
  82. config.memorySize = 0
  83. return
  84. }
  85. let jitSizeMib = config.jitCacheSize
  86. guard jitSizeMib >= 0 else {
  87. config.jitCacheSize = 0
  88. return
  89. }
  90. var totalDeviceMemory = ProcessInfo.processInfo.physicalMemory
  91. #if os(iOS) || os(visionOS)
  92. let availableMemory = UInt64(os_proc_available_memory())
  93. if availableMemory > 0 {
  94. totalDeviceMemory = availableMemory
  95. }
  96. #endif
  97. let actualJitSizeMib = jitSizeMib == 0 ? memorySizeMib / 4 : jitSizeMib
  98. let jitMirrorMultiplier = UTMCapabilities.current.contains(.hasJitEntitlements) ? 1 : 2;
  99. let estMemoryUsage = UInt64(memorySizeMib + jitMirrorMultiplier*actualJitSizeMib + baseUsageMib) * bytesInMib
  100. if Double(estMemoryUsage) > Double(totalDeviceMemory) * warningThreshold {
  101. warningMessage = WarningMessage.overallocatedRam(totalMib: totalDeviceMemory / bytesInMib, estimatedMib: estMemoryUsage / bytesInMib)
  102. }
  103. }
  104. func validateCpuCount(editing: Bool) {
  105. guard !editing else {
  106. return
  107. }
  108. guard config.cpuCount >= 0 else {
  109. config.cpuCount = 0
  110. return
  111. }
  112. }
  113. }
  114. private enum WarningMessage: Identifiable {
  115. case overallocatedRam(totalMib: UInt64, estimatedMib: UInt64)
  116. case resetSystem
  117. var id: Int {
  118. switch self {
  119. case .overallocatedRam(_, _):
  120. return 1
  121. case .resetSystem:
  122. return 2
  123. }
  124. }
  125. var localizedWarningTitle: String {
  126. switch self {
  127. case .overallocatedRam(_, _):
  128. return NSLocalizedString("Allocating too much memory will crash the VM.", comment: "VMConfigSystemView")
  129. case .resetSystem:
  130. return NSLocalizedString("This change will reset all settings", comment: "VMConfigSystemView")
  131. }
  132. }
  133. var localizedWarningMessage: String {
  134. switch self {
  135. case .overallocatedRam(let totalMib, let estimatedMib):
  136. let format = NSLocalizedString("Your device has %llu MB of memory and the estimated usage is %llu MB.", comment: "VMConfigSystemView")
  137. return String.localizedStringWithFormat(format, totalMib, estimatedMib)
  138. case .resetSystem:
  139. return NSLocalizedString("Any unsaved changes will be lost.", comment: "VMConfigSystemView")
  140. }
  141. }
  142. }
  143. private struct HardwareOptions: View {
  144. @Binding var config: UTMQemuConfigurationSystem
  145. @Binding var architecture: QEMUArchitecture
  146. @Binding var target: any QEMUTarget
  147. @Binding var warningMessage: WarningMessage?
  148. @EnvironmentObject private var data: UTMData
  149. @State private var isArchitectureFirstAppear: Bool = true
  150. @State private var isArchitectureSupported: Bool = true
  151. @State private var isTargetFirstAppear: Bool = true
  152. var body: some View {
  153. Section(header: Text("Hardware")) {
  154. VMConfigConstantPicker("Architecture", selection: $architecture)
  155. .onAppear {
  156. if isArchitectureFirstAppear {
  157. architecture = config.architecture
  158. }
  159. isArchitectureFirstAppear = false
  160. }
  161. .onChange(of: architecture) { newValue in
  162. if newValue != config.architecture {
  163. warningMessage = .resetSystem
  164. }
  165. }
  166. .onChange(of: config.architecture) { newValue in
  167. isArchitectureSupported = ConcreteVirtualMachine.isSupported(systemArchitecture: newValue)
  168. if newValue != architecture {
  169. architecture = newValue
  170. }
  171. }
  172. if !isArchitectureSupported {
  173. Text("The selected architecture is unsupported in this version of UTM.")
  174. .foregroundColor(.red)
  175. }
  176. VMConfigConstantPicker("System", selection: $target, type: config.architecture.targetType)
  177. .onAppear {
  178. if isTargetFirstAppear {
  179. target = config.target
  180. }
  181. isTargetFirstAppear = false
  182. }
  183. .onChange(of: target.rawValue) { newValue in
  184. if newValue != config.target.rawValue {
  185. warningMessage = .resetSystem
  186. }
  187. }
  188. .onChange(of: config.target.rawValue) { newValue in
  189. if newValue != target.rawValue {
  190. target = AnyQEMUConstant(rawValue: newValue)!
  191. }
  192. }
  193. }
  194. }
  195. }
  196. struct CPUFlagsOptions: View {
  197. let title: LocalizedStringKey
  198. @Binding var config: UTMQemuConfigurationSystem
  199. @Binding var flags: [any QEMUCPUFlag]
  200. @State private var showAllFlags: Bool = false
  201. var body: some View {
  202. let allFlags = config.architecture.cpuFlagType.allRawValues
  203. if config.cpu.rawValue != "default" && allFlags.count > 0 {
  204. Section(header: Text(title)) {
  205. if showAllFlags || flags.count > 0 {
  206. OptionsList {
  207. ForEach(allFlags) { flagStr in
  208. let flag = config.architecture.cpuFlagType.init(rawValue: flagStr)!
  209. let isFlagOn = Binding<Bool> { () -> Bool in
  210. flags.contains(where: { $0.rawValue == flag.rawValue })
  211. } set: { isOn in
  212. if isOn {
  213. flags.append(flag)
  214. } else {
  215. flags.removeAll(where: { $0.rawValue == flag.rawValue })
  216. }
  217. }
  218. if showAllFlags || isFlagOn.wrappedValue {
  219. Toggle(isOn: isFlagOn, label: {
  220. Text(flag.prettyValue)
  221. })
  222. }
  223. }
  224. }
  225. }
  226. Button {
  227. showAllFlags.toggle()
  228. } label: {
  229. if (showAllFlags) {
  230. Text("Hide Unused…")
  231. } else {
  232. Text("Show All…")
  233. }
  234. }
  235. }
  236. }
  237. }
  238. }
  239. struct OptionsList<Content>: View where Content: View {
  240. private var columns: [GridItem] = [
  241. GridItem(.fixed(150), spacing: 16),
  242. GridItem(.fixed(150), spacing: 16),
  243. GridItem(.fixed(150), spacing: 16),
  244. GridItem(.fixed(150), spacing: 16)
  245. ]
  246. var content: () -> Content
  247. init(content: @escaping () -> Content) {
  248. self.content = content
  249. }
  250. var body: some View {
  251. #if os(macOS)
  252. LazyVGrid(columns: columns, alignment: .leading) {
  253. content()
  254. }
  255. #else
  256. LazyVStack {
  257. content()
  258. }
  259. #endif
  260. }
  261. }
  262. struct VMConfigSystemView_Previews: PreviewProvider {
  263. @State static private var config = UTMQemuConfigurationSystem()
  264. static var previews: some View {
  265. VMConfigSystemView(config: $config, isResetConfig: .constant(false))
  266. #if os(macOS)
  267. .scrollable()
  268. #endif
  269. }
  270. }