diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 87259d840..5b29c80fa 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -538,6 +538,10 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; @@ -614,6 +618,10 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate index 80ae06ef0..8b26026b6 100644 Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 6b8d9f62c..529b37b6e 100644 --- a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -35,21 +35,5 @@ - - - - diff --git a/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift b/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift new file mode 100644 index 000000000..ace373c15 --- /dev/null +++ b/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift @@ -0,0 +1,58 @@ +// +// MTLHUD.swift +// MeloNX +// +// Created by Stossy11 on 26/11/2024. +// + +import Foundation + +class MTLHud { + + var canMetalHud: Bool { + return openMetalDylib() + } + + var isEnabled: Bool { + if let getenv = getenv("MTL_HUD_ENABLED") { + return String(cString: getenv).contains("1") + } + return false + } + + static let shared = MTLHud() + + private init() { + if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") { + enable() + } else { + disable() + } + } + + func openMetalDylib() -> Bool { + let path = "/usr/lib/libMTLHud.dylib" + + // Load the dynamic library + if dlopen(path, RTLD_NOW) != nil { + // Library loaded successfully + print("Library loaded from \(path)") + return true + } else { + // Handle error + if let error = String(validatingUTF8: dlerror()) { + print("Error loading library: \(error)") + } + return false + } + } + + + func enable() { + setenv("MTL_HUD_ENABLED", "1", 1) + } + + func disable() { + setenv("MTL_HUD_ENABLED", "0", 1) + } +} diff --git a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift index 53be4731d..a93288beb 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift @@ -34,9 +34,14 @@ class Ryujinx { @Published var controllerMap: [Controller] = [] + static let shared = Ryujinx() + + private init() {} + public struct Configuration : Codable { var gamepath: String var inputids: [String] + var resscale: Float var debuglogs: Bool var tracelogs: Bool var listinputids: Bool @@ -56,11 +61,13 @@ class Ryujinx { listinputids: Bool = false, fullscreen: Bool = true, memoryManagerMode: String = "HostMapped", - disableVSync: Bool = true, + disableVSync: Bool = false, disableShaderCache: Bool = false, - disableDockedMode: Bool = true, + disableDockedMode: Bool = false, enableTextureRecompression: Bool = true, - additionalArgs: [String] = []) { + additionalArgs: [String] = [], + resscale: Float = 1.00 + ) { self.gamepath = gamepath self.inputids = inputids self.debuglogs = debuglogs @@ -73,10 +80,10 @@ class Ryujinx { self.enableTextureRecompression = enableTextureRecompression self.additionalArgs = additionalArgs self.memoryManagerMode = memoryManagerMode + self.resscale = resscale } } - func start(with config: Configuration) throws { guard !isRunning else { @@ -84,6 +91,7 @@ class Ryujinx { } isRunning = true + // Start The Emulation on the main thread DispatchQueue.main.async { do { @@ -139,6 +147,10 @@ class Ryujinx { args.append(contentsOf: ["--exclusive-fullscreen-height", "720"]) } + if config.resscale != 1 { + args.append(contentsOf: ["--resolution-scale", String(config.resscale)]) + } + // Adding default args directly into additionalArgs if config.disableVSync { args.append("--disable-vsync") diff --git a/src/MeloNX/MeloNX/Views/ContentView.swift b/src/MeloNX/MeloNX/Views/ContentView.swift index 6ca2bb54e..85e050f2c 100644 --- a/src/MeloNX/MeloNX/Views/ContentView.swift +++ b/src/MeloNX/MeloNX/Views/ContentView.swift @@ -73,7 +73,7 @@ struct ContentView: View { GameListView(startemu: $game) .onAppear() { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in - controllersList = Ryujinx().getConnectedControllers() + controllersList = Ryujinx.shared.getConnectedControllers() controllersList.removeAll(where: { $0.id == "0" }) } } @@ -88,7 +88,7 @@ struct ContentView: View { } Section("Controller") { Button { - controllersList = Ryujinx().getConnectedControllers() + controllersList = Ryujinx.shared.getConnectedControllers() controllersList.removeAll(where: { $0.id == "0" }) } label: { Text("Refresh") @@ -128,10 +128,12 @@ struct ContentView: View { allocateSixGB() // Start the emulation + + print("Is MetalHud Enabled? " + (MTLHud.shared.isEnabled ? "yeah" : "nope")) do { setupVirtualController() - try Ryujinx().start(with: config) + try Ryujinx.shared.start(with: config) } catch { diff --git a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift index 74880129f..3902a69e2 100644 --- a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift @@ -17,47 +17,66 @@ struct SettingsView: View { ("SoftwarePageTable", "Software") ] + @AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false + var body: some View { - Form { - Section(header: Text("Graphics and Performance")) { - Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) - Toggle("Disable V-Sync", isOn: $config.disableVSync) - Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) - Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) - } - - Section(header: Text("Input Settings")) { - Toggle("List Input IDs", isOn: $config.listinputids) - // Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory) - Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) - } - - Section(header: Text("Logging Settings")) { - Toggle("Enable Debug Logs", isOn: $config.debuglogs) - Toggle("Enable Trace Logs", isOn: $config.tracelogs) - } - Section(header: Text("CPU Mode")) { - Picker("Memory Manager Mode", selection: $config.memoryManagerMode) { - ForEach(memoryManagerModes, id: \.0) { key, displayName in - Text(displayName).tag(key) + ScrollView { + VStack { + Section(header: Text("Graphics and Performance")) { + Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) + Toggle("Disable V-Sync", isOn: $config.disableVSync) + Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) + Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) + Resolution(value: $config.resscale) + Toggle("Enable Metal HUD", isOn: $metalHUDEnabled) + .onChange(of: metalHUDEnabled) { newValue in + if newValue { + MTLHud.shared.enable() + } else { + MTLHud.shared.disable() + } + } + } + + Section(header: Text("Input Settings")) { + Toggle("List Input IDs", isOn: $config.listinputids) + // Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory) + Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) + } + + Section(header: Text("Logging Settings")) { + Toggle("Enable Debug Logs", isOn: $config.debuglogs) + Toggle("Enable Trace Logs", isOn: $config.tracelogs) + } + Section(header: Text("CPU Mode")) { + HStack { + Spacer() + Picker("Memory Manager Mode", selection: $config.memoryManagerMode) { + ForEach(memoryManagerModes, id: \.0) { key, displayName in + Text(displayName).tag(key) + } + } + .pickerStyle(MenuPickerStyle()) // Dropdown style } } - .pickerStyle(MenuPickerStyle()) // Dropdown style - } - - Section(header: Text("Additional Settings")) { - //TextField("Game Path", text: $config.gamepath) - TextField("Additional Arguments", text: Binding( - get: { - config.additionalArgs.joined(separator: ", ") - }, - set: { newValue in - config.additionalArgs = newValue.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } - } - )) + + + Section(header: Text("Additional Settings")) { + //TextField("Game Path", text: $config.gamepath) + + TextField("Additional Arguments", text: Binding( + get: { + config.additionalArgs.joined(separator: ", ") + }, + set: { newValue in + config.additionalArgs = newValue.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } + } + )) + } } } + .padding() .onAppear { if let configs = loadSettings() { self.config = configs @@ -86,3 +105,56 @@ struct SettingsView: View { } } } + + +struct Resolution: View { + @Binding var value: Float + + var body: some View { + HStack { + Text("Resolution Scale (Custom):") + Spacer() + + Button(action: { + if value > 0.1 { // Prevent values going below 0.1 + value -= 0.10 + value = round(value * 1000) / 1000 // Round to two decimal places + } + print(value) + }) { + Text("-") + .frame(width: 30, height: 30) + .background(Color.gray.opacity(0.2)) + .cornerRadius(5) + } + + TextField("", value: $value, formatter: NumberFormatter.floatFormatter) + .multilineTextAlignment(.center) + .frame(width: 60) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.decimalPad) + + Button(action: { + value += 0.10 + value = round(value * 1000) / 1000 // Round to two decimal places + print(value) + }) { + Text("+") + .frame(width: 30, height: 30) + .background(Color.gray.opacity(0.2)) + .cornerRadius(5) + } + } + } +} + +extension NumberFormatter { + static var floatFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + formatter.allowsFloats = true + return formatter + } +}