diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 281c55f1f..e4c0aaef4 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -32,6 +32,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4E80A9852CD6F54500029585 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BD43C6212D1B248D003BBC42; + remoteInfo = com.Stossy11.MeloNX.RyujinxAg; + }; 4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4E80A9852CD6F54500029585 /* Project object */; @@ -46,13 +53,6 @@ remoteGlobalIDString = 4E80A98C2CD6F54500029585; remoteInfo = MeloNX; }; - 4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4E80A9852CD6F54500029585 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BD43C6212D1B248D003BBC42; - remoteInfo = com.Stossy11.MeloNX.RyujinxAg; - }; BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4E80A9852CD6F54500029585 /* Project object */; @@ -294,7 +294,7 @@ buildRules = ( ); dependencies = ( - 4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */, + 4E2953AC2D803BC9000497CD /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 4E80A98F2CD6F54500029585 /* MeloNX */, @@ -362,7 +362,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1610; + LastUpgradeCheck = 1620; TargetAttributes = { 4E80A98C2CD6F54500029585 = { CreatedOnToolsVersion = 16.1; @@ -453,7 +453,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/native/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n"; + shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/publish/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -482,6 +482,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */; + targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */; + }; 4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4E80A98C2CD6F54500029585 /* MeloNX */; @@ -492,11 +497,6 @@ target = 4E80A98C2CD6F54500029585 /* MeloNX */; targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */; }; - 4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */; - targetProxy = 4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */; - }; BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */; @@ -708,8 +708,12 @@ "$(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", ); - GCC_OPTIMIZATION_LEVEL = fast; + GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES; @@ -827,6 +831,17 @@ "$(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", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies", + "$(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 = "$(VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; @@ -905,8 +920,12 @@ "$(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", ); - GCC_OPTIMIZATION_LEVEL = fast; + GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES; @@ -1024,6 +1043,17 @@ "$(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", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies", + "$(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 = "$(VERSION)"; 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 55d0b6244..dc032eccc 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/xcshareddata/xcschemes/MeloNX.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme index 9f2439a96..df190bf1b 100644 --- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme +++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme @@ -1,6 +1,6 @@ diff --git a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme index eb2c0a6a5..a5e708fe3 100644 --- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme +++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/Ryujinx.xcscheme @@ -11,7 +11,7 @@ ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> 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)") + canMetalHud = true return true } else { - // Handle error if let error = String(validatingUTF8: dlerror()) { print("Error loading library: \(error)") } + canMetalHud = false return false } } diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift index 8d75f2231..8742486f6 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift @@ -360,18 +360,13 @@ class Ryujinx { } func fetchFirmwareVersion() -> String { - do { - let firmwareVersionPointer = installed_firmware_version() - if let pointer = firmwareVersionPointer { - let firmwareVersion = String(cString: pointer) - DispatchQueue.main.async { - self.firmwareversion = firmwareVersion - } - return firmwareVersion + let firmwareVersionPointer = installed_firmware_version() + if let pointer = firmwareVersionPointer { + let firmwareVersion = String(cString: pointer) + DispatchQueue.main.async { + self.firmwareversion = firmwareVersion } - - } catch { - print(error) + return firmwareVersion } return "0" @@ -500,62 +495,6 @@ class Ryujinx { } } - - func repeatuntilfindLayer() { - Task { @MainActor in - while self.metalLayer == nil { - let layer = self.getMetalLayer(nil) - - if layer != nil { - self.metalLayer = layer - break - } - - Thread.sleep(forTimeInterval: 0.1) - } - } - } - - - @MainActor - func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? { - var window = window - if window == nil { - window = SDL_GetWindowFromID(1) - } - - var windowInfo = SDL_SysWMinfo() - SDL_GetWindowWMInfo(window, &windowInfo) - - - guard let uiWindow = windowInfo.info.uikit.window, - let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else { - print("Unable to get root view") - return nil - } - - func findMetalLayer(in view: UIView) -> CAMetalLayer? { - if let metalLayer = view.layer as? CAMetalLayer { - return metalLayer - } - - for subview in view.subviews { - if let metalLayer = findMetalLayer(in: subview) { - return metalLayer - } - } - - return nil - } - - if let existingLayer = findMetalLayer(in: rootView) { - print("Found Metal Layer") - return existingLayer - } - print("found nothing") - return nil - } - static func log(_ message: String) { diff --git a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift index d6d314aae..481dd7724 100644 --- a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift +++ b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift @@ -32,7 +32,7 @@ struct LaunchGameIntentDef: AppIntent { let ryujinx = Ryujinx.shared.games - let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName)) + let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName)) let urlString = "melonx://game?name=\(name ?? gameName)" print(urlString) diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift index 96d27591d..1a154e869 100644 --- a/src/MeloNX/MeloNX/App/Models/Game.swift +++ b/src/MeloNX/MeloNX/App/Models/Game.swift @@ -63,22 +63,15 @@ public struct Game: Identifiable, Equatable, Hashable { } func createImage(from gameInfo: GameInfo) -> UIImage? { - // Access the struct let gameInfoValue = gameInfo - // Get the image data let imageSize = Int(gameInfoValue.ImageSize) guard imageSize > 0, imageSize <= 1024 * 1024 else { print("Invalid image size.") return nil } - // Convert the ImageData byte array to Swift's Data let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize) - - // Create a UIImage (or NSImage on macOS) - print(imageData) - return UIImage(data: imageData) } } diff --git a/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift b/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift new file mode 100644 index 000000000..27fa5749b --- /dev/null +++ b/src/MeloNX/MeloNX/App/Models/LatestVersionResponse.swift @@ -0,0 +1,38 @@ +// +// LatestVersionResponse.swift +// MeloNX +// +// Created by Stossy11 on 12/03/2025. +// + + + struct LatestVersionResponse: Codable { + let version_number: String + let version_number_stripped: String + let changelog: String + let download_link: String + + #if DEBUG + static let example1 = LatestVersionResponse( + version_number: "1.0.0", + version_number_stripped: "100", + changelog: """ + - Rewrite Display Code (SDL isn't used for display anymore) + - Add New Onboarding / Setup + - Better Performance + - Remove "SDL Window" option in settings + - Fix JIT Cache Regions + - Fix how JIT is detected in Settings + - Fix ABYX being swapped on controller. + - Settings are now a config.json file + - Fix Performance Overlay not showing when Virtual Controller is hidden + - Add displaying logs when Loading or in-game + - Fix Launching games from outside of the roms folder + - Add Waiting for JIT popup + - Fix spesific Games + - Added Back Herobrine ("You were supposed to be the hero, Bryan") + """, + download_link: "https://example.com" + ) + #endif +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift index 3dc9e510c..db6fdef89 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift @@ -6,12 +6,10 @@ // import SwiftUI -// import SDL2 import GameController import Darwin import UIKit import MetalKit -// import SDL struct MoltenVKSettings: Codable, Hashable { let string: String @@ -19,6 +17,8 @@ struct MoltenVKSettings: Codable, Hashable { } struct ContentView: View { + // MARK: - Properties + // Games @State private var game: Game? @@ -54,27 +54,26 @@ struct ContentView: View { @State private var isAnimating = false @State var isLoading = true @State var jitNotEnabled = false + + // MARK: - SDL + var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO // MARK: - Initialization init() { var defaultConfig = loadSettings() if defaultConfig == nil { saveSettings(config: .init(gamepath: "")) - defaultConfig = loadSettings() } - _config = State(initialValue: defaultConfig!) - let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings. + let defaultSettings: [MoltenVKSettings] = [ MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"), MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"), MoltenVKSettings(string: "MVK_DEBUG", value: "0"), - MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"), - MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"), - // MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"), - // Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64) + MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"), + MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"), MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"), ] @@ -85,229 +84,220 @@ struct ContentView: View { // MARK: - Body var body: some View { - if game != nil, !jitNotEnabled { - // This is when the game starts to stop the animation - ZStack { - if #available(iOS 16, *) { - EmulationView(startgame: $game) - .persistentSystemOverlays(.hidden) - } else { - EmulationView(startgame: $game) - } - - if isLoading { - ZStack { - Color.black - .opacity(0.8) - emulationView - .ignoresSafeArea(.all) - } - .edgesIgnoringSafeArea(.all) - .ignoresSafeArea(.all) - } - } - } else if game != nil, ignoreJIT { - ZStack { - if #available(iOS 16, *) { - EmulationView(startgame: $game) - .persistentSystemOverlays(.hidden) - } else { - EmulationView(startgame: $game) - } - - if isLoading { - ZStack { - Color.black - .opacity(0.8) - emulationView - .ignoresSafeArea(.all) - } - .edgesIgnoringSafeArea(.all) - .ignoresSafeArea(.all) - } - } - } else if game != nil { - Text("") - .sheet(isPresented: $jitNotEnabled) { - JITPopover() { - jitNotEnabled = false - } - .interactiveDismissDisabled() - } + if game != nil && (!jitNotEnabled || ignoreJIT) { + gameView + } else if game != nil && jitNotEnabled { + jitErrorView } else { - // This is the main menu view that includes the Settings and the Game Selector mainMenuView - .onAppear() { - quits = false - - loadSettings() - - isLoading = true - - initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts. - } - .onOpenURL() { url in - if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), - components.host == "game" { - if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { - - game = Ryujinx.shared.games.first(where: { $0.titleId == text }) - } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { - game = Ryujinx.shared.games.first(where: { $0.titleName == text }) - } - } - } } - } + // MARK: - View Components + + private var gameView: some View { + ZStack { + if #available(iOS 16, *) { + EmulationView(startgame: $game) + .persistentSystemOverlays(.hidden) + } else { + EmulationView(startgame: $game) + } + + if isLoading { + ZStack { + Color.black.opacity(0.8) + emulationView.ignoresSafeArea(.all) + } + .edgesIgnoringSafeArea(.all) + .ignoresSafeArea(.all) + } + } + } + + private var jitErrorView: some View { + Text("") + .sheet(isPresented: $jitNotEnabled) { + JITPopover() { + jitNotEnabled = false + } + .interactiveDismissDisabled() + } + } + + private var mainMenuView: some View { + MainTabView( + startemu: $game, + config: $config, + MVKconfig: $settings, + controllersList: $controllersList, + currentControllers: $currentControllers, + onscreencontroller: $onscreencontroller + ) + .onAppear { + quits = false + let _ = loadSettings() + isLoading = true + + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in + refreshControllersList() + } + + + print(MTLHud.shared.isEnabled) + + initControllerObservers() + + Air.play(AnyView( + VStack { + Image(systemName: "gamecontroller") + .font(.system(size: 300)) + .foregroundColor(.gray) + .padding(.bottom, 10) + + Text("Select Game") + .font(.system(size: 150)) + .bold() + } + )) + + checkJitStatus() + } + .onOpenURL { url in + handleDeepLink(url) + } + } + + private var emulationView: some View { + GeometryReader { screenGeometry in + ZStack { + gameLoadingContent(screenGeometry: screenGeometry) + + HStack{ + + VStack { + if showlogsloading { + LogFileView(isfps: true) + .frame(alignment: .topLeading) + } + + Spacer() + } + + Spacer() + } + } + } + } + + // MARK: - Helper Methods + + private func gameLoadingContent(screenGeometry: GeometryProxy) -> some View { + HStack(spacing: screenGeometry.size.width * 0.04) { + if let icon = game?.icon { + Image(uiImage: icon) + .resizable() + .frame( + width: min(screenGeometry.size.width * 0.25, 250), + height: min(screenGeometry.size.width * 0.25, 250) + ) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5) + } + + VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) { + Text("Loading \(game?.titleName ?? "Game")") + .font(.system(size: min(screenGeometry.size.width * 0.04, 32))) + .foregroundColor(.white) + + loadingProgressBar(screenGeometry: screenGeometry) + } + } + .padding(.horizontal, screenGeometry.size.width * 0.06) + .padding(.vertical, screenGeometry.size.height * 0.05) + .position( + x: screenGeometry.size.width / 2, + y: screenGeometry.size.height * 0.5 + ) + } + + private func loadingProgressBar(screenGeometry: GeometryProxy) -> some View { + GeometryReader { geometry in + let containerWidth = min(screenGeometry.size.width * 0.35, 350) + + ZStack(alignment: .leading) { + Rectangle() + .cornerRadius(10) + .frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12)) + .foregroundColor(.gray.opacity(0.3)) + .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2) + + Rectangle() + .cornerRadius(10) + .frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12)) + .foregroundColor(.blue) + .shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2) + .offset(x: isAnimating ? containerWidth : -clumpWidth) + .animation( + Animation.linear(duration: 1.0) + .repeatForever(autoreverses: false), + value: isAnimating + ) + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .onAppear { + isAnimating = true + setupEmulation() + + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in + if get_current_fps() != 0 { + withAnimation { + isLoading = false + isAnimating = false + } + timer.invalidate() + } + } + } + } + .frame(height: min(screenGeometry.size.height * 0.015, 12)) + .frame(width: min(screenGeometry.size.width * 0.35, 350)) + } + + private func initializeSDL() { + setMoltenVKSettings() + SDL_SetMainReady() + SDL_iPhoneSetEventPump(SDL_TRUE) + SDL_Init(sdlInitFlags) + initialize() + } private func initControllerObservers() { NotificationCenter.default.addObserver( forName: .GCControllerDidConnect, object: nil, - queue: .main) { notification in - if let controller = notification.object as? GCController { - print("Controller connected: \(controller.productCategory)") - nativeControllers[controller] = .init(controller) - refreshControllersList() - } + queue: .main + ) { notification in + if let controller = notification.object as? GCController { + print("Controller connected: \(controller.productCategory)") + nativeControllers[controller] = .init(controller) + refreshControllersList() + } } - NotificationCenter.default.addObserver( forName: .GCControllerDidDisconnect, object: nil, - queue: .main) { notification in - if let controller = notification.object as? GCController { - print("Controller disconnected: \(controller.productCategory)") - nativeControllers[controller]?.cleanup() - nativeControllers[controller] = nil - refreshControllersList() - } - } - } - - // MARK: - View Components - private var emulationView: some View { - GeometryReader { screenGeometry in - ZStack { - HStack(spacing: screenGeometry.size.width * 0.04) { - if let icon = game?.icon { - Image(uiImage: icon) - .resizable() - .frame( - width: min(screenGeometry.size.width * 0.25, 250), - height: min(screenGeometry.size.width * 0.25, 250) - ) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5) - } - - VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) { - Text("Loading \(game?.titleName ?? "Game")") - .font(.system(size: min(screenGeometry.size.width * 0.04, 32))) - .foregroundColor(.white) - - GeometryReader { geometry in - let containerWidth = min(screenGeometry.size.width * 0.35, 350) - - ZStack(alignment: .leading) { - Rectangle() - .cornerRadius(10) - .frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12)) - .foregroundColor(.gray.opacity(0.3)) - .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2) - - Rectangle() - .cornerRadius(10) - .frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12)) - .foregroundColor(.blue) - .shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2) - .offset(x: isAnimating ? containerWidth : -clumpWidth) - .animation( - Animation.linear(duration: 1.0) - .repeatForever(autoreverses: false), - value: isAnimating - ) - } - .clipShape(RoundedRectangle(cornerRadius: 16)) - .onAppear { - isAnimating = true - - setupEmulation() - - - Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in - if get_current_fps() != 0 { - withAnimation { - isLoading = false - - isAnimating = false - } - - - - timer.invalidate() - } - } - } - } - .frame(height: min(screenGeometry.size.height * 0.015, 12)) - .frame(width: min(screenGeometry.size.width * 0.35, 350)) - } - } - .padding(.horizontal, screenGeometry.size.width * 0.06) - .padding(.vertical, screenGeometry.size.height * 0.05) - .position( - x: screenGeometry.size.width / 2, - y: screenGeometry.size.height * 0.5 - ) - } - - if showlogsloading { - LogFileView(isfps: true) - .frame(alignment: .topLeading) + queue: .main + ) { notification in + if let controller = notification.object as? GCController { + print("Controller disconnected: \(controller.productCategory)") + nativeControllers[controller]?.cleanup() + nativeControllers[controller] = nil + refreshControllersList() } } } - - private var mainMenuView: some View { - MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller) - .onAppear() { - Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in - refreshControllersList() - } - - Air.play(AnyView( - VStack { - Image(systemName: "gamecontroller") - .font(.system(size: 300)) - .foregroundColor(.gray) - .padding(.bottom, 10) - - Text("Select Game") - .font(.system(size: 150)) - .bold() - } - )) - - jitNotEnabled = !isJITEnabled() - if jitNotEnabled { - useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT") - } - } - } - - // MARK: - Helper Methods - var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video. - private func initializeSDL() { - setMoltenVKSettings() - SDL_SetMainReady() // Sets SDL Ready - SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true - SDL_Init(SdlInitFlags) // Initialises SDL2 - initialize() - } private func setupEmulation() { isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) @@ -330,8 +320,7 @@ struct ContentView: View { currentControllers = [] if controllersList.count == 1 { - let controller = controllersList[0] - currentControllers.append(controller) + currentControllers.append(controllersList[0]) } else if (controllersList.count - 1) >= 1 { for controller in controllersList { if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) { @@ -341,29 +330,18 @@ struct ContentView: View { } } - - private func start(displayid: UInt32) { guard let game else { return } config.gamepath = game.fileURL.path config.inputids = Array(Set(currentControllers.map(\.id))) - if mVKPreFillBuffer { - let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2") - setenv(setting.string, setting.value, 1) - } - - if syncqsubmits { - let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2") - setenv(setting.string, setting.value, 1) - } + configureEnvironmentVariables() if config.inputids.isEmpty { config.inputids.append("0") } - do { try Ryujinx.shared.start(with: config) } catch { @@ -371,14 +349,45 @@ struct ContentView: View { } } + private func configureEnvironmentVariables() { + if mVKPreFillBuffer { + setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1) + } + + if syncqsubmits { + setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1) + } + } - - // Sets MoltenVK Environment Variables private func setMoltenVKSettings() { settings.forEach { setting in setenv(setting.string, setting.value, 1) } } + + private func checkJitStatus() { + jitNotEnabled = !isJITEnabled() + if jitNotEnabled { + if useTrollStore { + askForJIT() + } else if jitStreamerEB { + enableJITEB() + } else { + print("no JIT") + } + } + } + + private func handleDeepLink(_ url: URL) { + if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + components.host == "game" { + if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { + game = Ryujinx.shared.games.first(where: { $0.titleId == text }) + } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { + game = Ryujinx.shared.games.first(where: { $0.titleName == text }) + } + } + } } extension Array { diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift index 44d786d25..9f9576bb0 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MetalView.swift @@ -15,7 +15,7 @@ struct MetalView: UIViewRepresentable { func makeUIView(context: Context) -> UIView { if Ryujinx.shared.emulationUIView == nil { - var view = MeloMTKView() + let view = MeloMTKView() guard let metalLayer = view.layer as? CAMetalLayer else { fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer") @@ -34,13 +34,19 @@ struct MetalView: UIViewRepresentable { return view } - let uiview = UIView() - - uiview.layer.addSublayer(Ryujinx.shared.metalLayer!) - - uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale - - return uiview + if Double(UIDevice.current.systemVersion)! < 17.0 { + + let uiview = MTKView() + let layer = Ryujinx.shared.metalLayer! + + layer.frame = uiview.bounds + + uiview.layer.addSublayer(layer) + + return uiview + } else { + return Ryujinx.shared.emulationUIView! + } } func updateUIView(_ uiView: UIView, context: Context) { diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift index 03a85547f..e4cbaa44e 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/TouchView.swift @@ -10,7 +10,7 @@ import MetalKit struct TouchView: UIViewRepresentable { func makeUIView(context: Context) -> UIView { - var view = MeloMTKView() + let view = MeloMTKView() return view } diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift index eab988044..be8cbb270 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift @@ -258,7 +258,7 @@ struct GameLibraryView: View { let fileExtension = (url.pathExtension as NSString).utf8String let extensionPtr = UnsafeMutablePointer(mutating: fileExtension) - var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) + let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url) diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift index 1d7fb0d2e..1127122c9 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift @@ -183,7 +183,7 @@ struct SettingsView: View { .padding(.vertical, 8) Toggle(isOn: $performacehud) { - labelWithIcon("Performance Overlay", iconName: "speedometer") + labelWithIcon("Custom Performance Overlay", iconName: "speedometer") } .tint(.blue) } header: { @@ -452,7 +452,8 @@ struct SettingsView: View { .tint(.blue) .contextMenu { Button { - if let mainWindow = UIApplication.shared.windows.last { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let mainWindow = windowScene.windows.last { let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert) let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in @@ -481,7 +482,8 @@ struct SettingsView: View { }.tint(.blue) .contextMenu() { Button { - if let mainWindow = UIApplication.shared.windows.last { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let mainWindow = windowScene.windows.last { let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert) let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil) @@ -546,7 +548,7 @@ struct SettingsView: View { if ProcessInfo.processInfo.isiOSAppOnMac { labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill") } else { - labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill") + labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill") } labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo") @@ -577,7 +579,16 @@ struct SettingsView: View { Spacer() Text("\(String(Int(getpagesize())))") .foregroundColor(.secondary) - + } + + if MTLHud.shared.canMetalHud { + Toggle(isOn: $metalHUDEnabled) { + labelWithIcon("Metal Performance HUD", iconName: "speedometer") + } + .tint(.blue) + .onChange(of: metalHUDEnabled) { newValue in + MTLHud.shared.toggle() + } } Toggle(isOn: $ignoreJIT) { @@ -617,7 +628,7 @@ struct SettingsView: View { .textCase(nil) .headerProminence(.increased) } footer: { - Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")") + Text("For advanced users. See page size or add custom arguments for experimental features, \"Metal Performance HUD\" is not needed if you have it enabled in settings. \n \n\(gamepo ? "the cake is a lie" : "")") } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift new file mode 100644 index 000000000..a9f98de55 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift @@ -0,0 +1,60 @@ +// +// MeloNXUpdateSheet.swift +// MeloNX +// +// Created by Stossy11 and Bella on 12/03/2025. +// + +import SwiftUI + +struct MeloNXUpdateSheet: View { + let updateInfo: LatestVersionResponse + @Binding var isPresented: Bool + + var body: some View { + iOSNav { + VStack { + Text("Version \(updateInfo.version_number) is available. You are currently on Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown").") + + VStack { + Text("Changelog:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.headline) + + ScrollView { + Text(updateInfo.changelog) + .padding() + } + .frame(maxHeight: 400) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + .padding(.top, 15) + + + Spacer() + Button(action: { + if let url = URL(string: updateInfo.download_link) { + UIApplication.shared.open(url) + } + }) { + Text("Download Now") + .font(.title3) + .bold() + .frame(width: 300, height: 40) + } + .buttonStyle(.borderedProminent) + .frame(alignment: .bottom) + } + .padding(.horizontal) + .navigationTitle("Version \(updateInfo.version_number) Available!") + .toolbar { + Button(action: { + isPresented = false + }) { + Text("Close") + } + } + } + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift similarity index 100% rename from src/MeloNX/MeloNX/App/Views/Main/Updates/GameDLCManagerSheet.swift rename to src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift similarity index 100% rename from src/MeloNX/MeloNX/App/Views/Main/Updates/GameUpdateManagerSheet.swift rename to src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift diff --git a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift index 4f36da0b4..0bc1b5090 100644 --- a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift +++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift @@ -18,261 +18,86 @@ struct MeloNXApp: App { @Environment(\.scenePhase) var scenePhase @State var alert: UIAlertController? = nil + @State var showOutOfDateSheet = false + @State var updateInfo: LatestVersionResponse? = nil + @State var finished = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false var body: some Scene { WindowGroup { - ZStack { - if showed || DRM != 1 { - - if finishedStorage { - ContentView() - } else { - SetupView(finished: $finished) - .onChange(of: finished) { newValue in - withAnimation { - withAnimation { - finishedStorage = newValue - } - } - } - } - } else { - Group { - VStack { - Spacer() - - HStack { - Text("Loading...") - ProgressView() - } - Spacer() - - Text(UIDevice.current.identifierForVendor?.uuidString ?? "") - } - } + if finishedStorage { + ContentView() .onAppear { - initR() + checkLatestVersion() } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(1)) - .foregroundColor(.white) - } - } - } - } - - func initR() { - if DRM == 1 { - DispatchQueue.main.async { [self] in - // drmcheck() - InitializeRyujinx() { bool in - if bool { - print("Ryujinx Files Initialized Successfully") - DispatchQueue.main.async { [self] in - withAnimation { - showed = true + .sheet(isPresented: Binding( + get: { showOutOfDateSheet && updateInfo != nil }, + set: { newValue in + if !newValue { + showOutOfDateSheet = false + updateInfo = nil } - - Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in - InitializeRyujinx() { bool in - if !bool, (scenePhase != .background || scenePhase == .inactive) { - withAnimation { - showed = false - } - if !(alert?.isViewLoaded ?? false) { - alert = showDMCAAlert() - } - } else { - DispatchQueue.main.async { - alert?.dismiss(animated: true) - showed = true - } - } - } - } - } - - } else { - showDMCAAlert() + )) { + if let updateInfo = updateInfo { + MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet) + } } - - } - - } - - } - - } - - - func showAlert() -> UIAlertController? { - // Create the alert controller - if let mainWindow = UIApplication.shared.windows.last { - let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert) - - // Add a text field to the alert - alertController.addTextField { textField in - textField.placeholder = "Enter key here" - } - - // Add the "OK" action - let okAction = UIAlertAction(title: "OK", style: .default) { _ in - // Get the text entered in the text field - if let textField = alertController.textFields?.first, let enteredText = textField.text { - print("Entered text: \(enteredText)") - UserDefaults.standard.set(enteredText, forKey: "MeloDRMID") - // drmcheck() { bool in - // if bool { - // showed = true - // } else { - // exit(0) - // } - // } - } - } - alertController.addAction(okAction) - - // Add a "Cancel" action - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - - // Present the alert - mainWindow.rootViewController!.present(alertController, animated: true, completion: nil) - - return alertController - } else { - return nil - } - } - - -} - -func showDMCAAlert() -> UIAlertController? { - if let mainWindow = UIApplication.shared.windows.first { - let alertController = UIAlertController(title: "Unauthorized Copy Notice", message: "This app was illegally leaked. Please report the download on the MeloNX Discord. In the meantime, check out Pomelo! \n -Stossy11", preferredStyle: .alert) - - DispatchQueue.main.async { - mainWindow.rootViewController!.present(alertController, animated: true, completion: nil) - } - - return alertController - } else { - // uhoh - return nil - } -} - -/* -func drmcheck(completion: @escaping (Bool) -> Void) { - if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() { - if let value = UserDefaults.standard.string(forKey: "MeloDRMID") { - if let url = URL(string: "https://mx.stossy11.com/auth/\(value)/\(base64device)") { - print(url) - // Create a URLSession - let session = URLSession.shared - - // Create a data task - let task = session.dataTask(with: url) { data, response, error in - // Handle errors - if let error = error { - exit(0) + } else { + SetupView(finished: $finished) + .onChange(of: finished) { newValue in + withAnimation { + withAnimation { + finishedStorage = newValue + } + } } - - // Check response and data - if let response = response as? HTTPURLResponse, response.statusCode == 200 { - print("Successfully Recieved API Data") - completion(true) - } else if let response = response as? HTTPURLResponse, response.statusCode == 201 { - print("Successfully Created Auth UUID") - completion(true) - } else { - completion(false) - } - } - - // Start the task - task.resume() } - } else { - completion(false) } - } else { - completion(false) } -} -*/ - -func InitializeRyujinx(completion: @escaping (Bool) -> Void) { - let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v" - - guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else { - completion(false) - return - } - - - - if (detectRoms(path: path) != value) { - completion(false) - } - - let configuration = URLSessionConfiguration.default - configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData - configuration.urlCache = nil - - let session = URLSession(configuration: configuration) - - guard let url = URL(string: addFolders(path)!) else { - completion(false) - return - } - - let task = session.dataTask(with: url) { data, response, error in - if error != nil { - completion(false) - } + func checkLatestVersion() { + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" + let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "") + #if DEBUG + let urlString = "http://192.168.178.116:8000/api/latest_release" + #else + let urlString = "https://melonx.org/api/latest_release" + #endif - guard let httpResponse = response as? HTTPURLResponse else { - completion(false) + guard let url = URL(string: urlString) else { + print("Invalid URL") return } - if httpResponse.statusCode == 200 { - completion(true) - } else { - completion(false) + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + print("Error checking for new version: \(error)") + return + } + + guard let data = data else { + print("No data received") + return + } + + do { + let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data) + let latestAPIVersionStripped = latestVersionResponse.version_number_stripped + + if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 { + DispatchQueue.main.async { + updateInfo = latestVersionResponse + showOutOfDateSheet = true + } + } + } catch { + print("Failed to decode response: \(error)") + } } - return - } - task.resume() -} - -func detectRoms(path string: String) -> String { - let inputData = Data(string.utf8) - let romHash = SHA256.hash(data: inputData) - return romHash.compactMap { String(format: "%02x", $0) }.joined() -} - - - -func addFolders(_ folderPath: String) -> String? { - let fileManager = FileManager.default - if let data = Data(base64Encoded: folderPath), - let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString { - return decodedString + "auth/" + fileURL + "/" - } - return nil -} - -extension String { - - func print() { - Swift.print(self) + + task.resume() } } diff --git a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift index e71e1ac10..859c40a2c 100644 --- a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift +++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift @@ -65,7 +65,8 @@ struct SetupView: View { initialize() finished = false keysImported = Ryujinx.shared.checkIfKeysImported() - firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") + print((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0)) + firmImported = ((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0) != 0) } } @@ -116,6 +117,9 @@ struct SetupView: View { .font(.title) .fontWeight(.bold) .foregroundColor(.primary) + .onTapGesture(count: 2) { + showSkipAlert = true + } Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.") .font(.subheadline) @@ -365,8 +369,9 @@ struct SetupView: View { Ryujinx.shared.installFirmware(firmwarePath: fileURL.path) + print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0) - firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") + firmImported = ((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0) != 0) alertMessage = "Firmware installed successfully" showAlert = true diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib index c1b12b543..4f3386894 100755 Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib differ diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK index 495d9fb19..4f3386894 100755 Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK and b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK differ diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 1b404a06a..1f60f5a08 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging TamperMachine, UI, Vic, + Memory, } } diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs index a71074995..5a76bb291 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs @@ -4,6 +4,7 @@ using Ryujinx.Memory; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Ryujinx.Cpu.LightningJit.Cache { @@ -21,12 +22,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache { private readonly ReservedRegion _region; private readonly CacheMemoryAllocator _cacheAllocator; + public readonly IJitMemoryAllocator Allocator; - public CacheMemoryAllocator Allocator => _cacheAllocator; + public CacheMemoryAllocator CacheAllocator => _cacheAllocator; public IntPtr Pointer => _region.Block.Pointer; public MemoryCache(IJitMemoryAllocator allocator, ulong size) { + Allocator = allocator; _region = new(allocator, size); _cacheAllocator = new((int)size); } @@ -101,9 +104,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache private readonly IStackWalker _stackWalker; private readonly Translator _translator; - private readonly MemoryCache _sharedCache; - private readonly MemoryCache _localCache; - private readonly PageAlignedRangeList _pendingMap; + private readonly List _sharedCaches; + private readonly List _localCaches; + private readonly Dictionary _pendingMaps; private readonly object _lock; class ThreadLocalCacheEntry @@ -111,13 +114,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache public readonly int Offset; public readonly int Size; public readonly IntPtr FuncPtr; + public readonly int CacheIndex; private int _useCount; - public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr) + public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr, int cacheIndex) { Offset = offset; Size = size; FuncPtr = funcPtr; + CacheIndex = cacheIndex; _useCount = 0; } @@ -134,12 +139,87 @@ namespace Ryujinx.Cpu.LightningJit.Cache { _stackWalker = stackWalker; _translator = translator; - _sharedCache = new(allocator, SharedCacheSize); - _localCache = new(allocator, LocalCacheSize); - _pendingMap = new(_sharedCache.ReprotectAsRx, RegisterFunction); + _sharedCaches = new List { new(allocator, SharedCacheSize) }; + _localCaches = new List { new(allocator, LocalCacheSize) }; + _pendingMaps = new Dictionary(); _lock = new(); } + private PageAlignedRangeList GetPendingMapForCache(int cacheIndex) + { + ulong cacheKey = (ulong)cacheIndex; + if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap)) + { + pendingMap = new PageAlignedRangeList( + (offset, size) => _sharedCaches[cacheIndex].ReprotectAsRx(offset, size), + (address, func) => RegisterFunction(address, func)); + _pendingMaps[cacheKey] = pendingMap; + } + return pendingMap; + } + + private bool HasInAnyPendingMap(ulong guestAddress) + { + foreach (var pendingMap in _pendingMaps.Values) + { + if (pendingMap.Has(guestAddress)) + { + return true; + } + } + return false; + } + + private int AllocateInSharedCache(int codeLength) + { + for (int i = 0; i < _sharedCaches.Count; i++) + { + try + { + return (i << 28) | _sharedCaches[i].Allocate(codeLength); + } + catch (OutOfMemoryException) + { + // Try next cache + } + } + + // All existing caches are full, create a new one + lock (_lock) + { + var allocator = _sharedCaches[0].Allocator; + _sharedCaches.Add(new(allocator, SharedCacheSize)); + return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength); + } + } + + private int AllocateInLocalCache(int codeLength) + { + for (int i = 0; i < _localCaches.Count; i++) + { + try + { + return (i << 28) | _localCaches[i].Allocate(codeLength); + } + catch (OutOfMemoryException) + { + + } + } + + lock (_lock) + { + var allocator = _localCaches[0].Allocator; + _localCaches.Add(new(allocator, LocalCacheSize)); + return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength); + } + } + + private static (int cacheIndex, int offset) SplitCacheOffset(int combinedOffset) + { + return (combinedOffset >> 28, combinedOffset & 0xFFFFFFF); + } + public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan code, ulong guestAddress, ulong guestSize) { if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr)) @@ -149,16 +229,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache lock (_lock) { - if (!_pendingMap.Has(guestAddress) && !_translator.Functions.ContainsKey(guestAddress)) + if (!HasInAnyPendingMap(guestAddress) && !_translator.Functions.ContainsKey(guestAddress)) { - int funcOffset = _sharedCache.Allocate(code.Length); - - funcPtr = _sharedCache.Pointer + funcOffset; + int combinedOffset = AllocateInSharedCache(code.Length); + var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset); + + MemoryCache cache = _sharedCaches[cacheIndex]; + funcPtr = cache.Pointer + funcOffset; code.CopyTo(new Span((void*)funcPtr, code.Length)); TranslatedFunction function = new(funcPtr, guestSize); - - _pendingMap.Add(funcOffset, code.Length, guestAddress, function); + + GetPendingMapForCache(cacheIndex).Add(funcOffset, code.Length, guestAddress, function); } ClearThreadLocalCache(framePointer); @@ -171,25 +253,63 @@ namespace Ryujinx.Cpu.LightningJit.Cache { lock (_lock) { + int cacheIndex; + int funcOffset; + IntPtr mappedFuncPtr = IntPtr.Zero; + + for (cacheIndex = 0; cacheIndex < _sharedCaches.Count; cacheIndex++) + { + try + { + var pendingMap = GetPendingMapForCache(cacheIndex); + + pendingMap.Pad(_sharedCaches[cacheIndex].CacheAllocator); + + int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); + funcOffset = _sharedCaches[cacheIndex].Allocate(sizeAligned); + + Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0); + + IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset; + code.CopyTo(new Span((void*)funcPtr1, code.Length)); + + _sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned); + + return funcPtr1; + } + catch (OutOfMemoryException) + { + // Try next cache + } + } + + // All existing caches are full, create a new one + var allocator = _sharedCaches[0].Allocator; + var newCache = new MemoryCache(allocator, SharedCacheSize); + _sharedCaches.Add(newCache); + cacheIndex = _sharedCaches.Count - 1; + + var newPendingMap = GetPendingMapForCache(cacheIndex); + // Ensure we will get an aligned offset from the allocator. - _pendingMap.Pad(_sharedCache.Allocator); - - int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); - int funcOffset = _sharedCache.Allocate(sizeAligned); + newPendingMap.Pad(newCache.CacheAllocator); + int newSizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); + funcOffset = newCache.Allocate(newSizeAligned); + Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0); - IntPtr funcPtr = _sharedCache.Pointer + funcOffset; + IntPtr funcPtr = newCache.Pointer + funcOffset; code.CopyTo(new Span((void*)funcPtr, code.Length)); - _sharedCache.ReprotectAsRx(funcOffset, sizeAligned); + newCache.ReprotectAsRx(funcOffset, newSizeAligned); return funcPtr; } } private bool TryGetThreadLocalFunction(ulong guestAddress, out IntPtr funcPtr) - { + { if ((_threadLocalCache ??= new()).TryGetValue(guestAddress, out var entry)) { if (entry.IncrementUseCount() >= MinCallsForPad) @@ -200,7 +320,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache lock (_lock) { - _pendingMap.Pad(_sharedCache.Allocator); + foreach (var pendingMap in _pendingMaps.Values) + { + // Get the cache index from the pendingMap key + if (_pendingMaps.FirstOrDefault(x => x.Value == pendingMap).Key is ulong cacheIndex) + { + // Use the correct shared cache for padding based on the cache index + pendingMap.Pad(_sharedCaches[(int)cacheIndex].CacheAllocator); + } + } } } @@ -224,12 +352,36 @@ namespace Ryujinx.Cpu.LightningJit.Cache return; } - IEnumerable callStack = _stackWalker.GetCallStack( - framePointer, - _localCache.Pointer, - LocalCacheSize, - _sharedCache.Pointer, - SharedCacheSize); + IntPtr[] cachePointers = new IntPtr[_localCaches.Count]; + int[] cacheSizes = new int[_localCaches.Count]; + + for (int i = 0; i < _localCaches.Count; i++) + { + cachePointers[i] = _localCaches[i].Pointer; + cacheSizes[i] = LocalCacheSize; + } + + IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count]; + int[] sharedSizes = new int[_sharedCaches.Count]; + + for (int i = 0; i < _sharedCaches.Count; i++) + { + sharedPointers[i] = _sharedCaches[i].Pointer; + sharedSizes[i] = SharedCacheSize; + } + + // Iterate over the arrays and pass each element to GetCallStack + IEnumerable callStack = null; + for (int i = 0; i < _localCaches.Count; i++) + { + callStack = _stackWalker.GetCallStack( + framePointer, + cachePointers[i], // Passing each individual cachePointer + cacheSizes[i], // Passing each individual cacheSize + sharedPointers[i], // Passing each individual sharedPointer + sharedSizes[i] // Passing each individual sharedSize + ); + } List<(ulong, ThreadLocalCacheEntry)> toDelete = new(); @@ -237,7 +389,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache { // We only want to delete if the function is already on the shared cache, // otherwise we will keep translating the same function over and over again. - bool canDelete = !_pendingMap.Has(address); + bool canDelete = !HasInAnyPendingMap(address); if (!canDelete) { continue; @@ -267,12 +419,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache _threadLocalCache.Remove(address); int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize); + var (cacheIndex, offset) = SplitCacheOffset(entry.Offset); - _localCache.Free(entry.Offset, sizeAligned); - _localCache.ReprotectAsRw(entry.Offset, sizeAligned); + _localCaches[cacheIndex].Free(offset, sizeAligned); + _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned); } } + public void ClearEntireThreadLocalCache() { // Thread is exiting, delete everything. @@ -287,9 +441,10 @@ namespace Ryujinx.Cpu.LightningJit.Cache foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache) { int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize); + var (cacheIndex, offset) = SplitCacheOffset(entry.Offset); - _localCache.Free(entry.Offset, sizeAligned); - _localCache.ReprotectAsRw(entry.Offset, sizeAligned); + _localCaches[cacheIndex].Free(offset, sizeAligned); + _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned); } _threadLocalCache.Clear(); @@ -299,16 +454,17 @@ namespace Ryujinx.Cpu.LightningJit.Cache private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan code, ulong guestAddress) { int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); - int funcOffset = _localCache.Allocate(alignedSize); + int combinedOffset = AllocateInLocalCache(alignedSize); + var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset); Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0); - IntPtr funcPtr = _localCache.Pointer + funcOffset; + IntPtr funcPtr = _localCaches[cacheIndex].Pointer + funcOffset; code.CopyTo(new Span((void*)funcPtr, code.Length)); - (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr)); + (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex)); - _localCache.ReprotectAsRx(funcOffset, alignedSize); + _localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize); return funcPtr; } @@ -326,8 +482,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache { if (disposing) { - _localCache.Dispose(); - _sharedCache.Dispose(); + foreach (var cache in _localCaches) + { + cache.Dispose(); + } + + foreach (var cache in _sharedCaches) + { + cache.Dispose(); + } + + _localCaches.Clear(); + _sharedCaches.Clear(); } } @@ -337,4 +503,4 @@ namespace Ryujinx.Cpu.LightningJit.Cache GC.SuppressFinalize(this); } } -} +} \ No newline at end of file diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index 796fc3bec..d067c489d 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -769,46 +769,119 @@ namespace Ryujinx.Graphics.Vulkan private void SetData(ReadOnlySpan data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle? region = null) { + const int MaxChunkSize = 1024 * 1024 * 16; // 16MB chunks + int bufferDataLength = GetBufferDataLength(data.Length); - - using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength); - - Auto imageAuto = GetImage(); - - // Load texture data inline if the texture has been used on the current command buffer. - - bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer); - - var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer(); - - if (loadInline) + + if (bufferDataLength <= MaxChunkSize) { - _gd.PipelineInternal.EndRenderPass(); + ProcessChunk(data, layer, level, layers, levels, singleSlice, region); + return; } - - CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data); - - var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; - var image = imageAuto.Get(cbs).Value; - - if (region.HasValue) + + if (!region.HasValue && !singleSlice && layers > 1) { - CopyFromOrToBuffer( - cbs.CommandBuffer, - buffer, - image, - bufferDataLength, - false, - layer, - level, - region.Value.X, - region.Value.Y, - region.Value.Width, - region.Value.Height); + int layerSize = data.Length / layers; + int offset = 0; + + for (int i = 0; i < layers; i++) + { + int currentLayer = layer + i; + int currentLayerSize = Math.Min(layerSize, data.Length - offset); + var layerData = data.Slice(offset, currentLayerSize); + + ProcessChunk(layerData, currentLayer, level, 1, levels, true); + offset += layerSize; + + if (offset >= data.Length) + break; + } + } + else if (region.HasValue) + { + var rect = region.Value; + int dataPerPixel = data.Length / (rect.Width * rect.Height); + int rowStride = rect.Width * dataPerPixel; + + int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride); + int originalHeight = rect.Height; + int currentY = rect.Y; + int offset = 0; + + while (currentY < rect.Y + originalHeight) + { + int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY); + var chunkRegion = new Rectangle(rect.X, currentY, rect.Width, chunkHeight); + + int chunkSize = chunkHeight * rowStride; + int safeChunkSize = Math.Min(chunkSize, data.Length - offset); + var chunkData = data.Slice(offset, safeChunkSize); + + ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion); + + currentY += chunkHeight; + offset += chunkSize; + + if (offset >= data.Length) + break; + } } else { - CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice); + ProcessChunk(data, layer, level, layers, levels, singleSlice, region); + } + + void ProcessChunk(ReadOnlySpan chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle? chunkRegion = null) + { + int chunkBufferLength = GetBufferDataLength(chunkData.Length); + + using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength); + + using (var imageAuto = GetImage()) + { + bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer); + var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer(); + + if (loadInline) + { + _gd.PipelineInternal.EndRenderPass(); + } + + CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData); + + var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; + var image = imageAuto.Get(cbs).Value; + + if (chunkRegion.HasValue) + { + CopyFromOrToBuffer( + cbs.CommandBuffer, + buffer, + image, + chunkBufferLength, + false, + chunkLayer, + chunkLevel, + chunkRegion.Value.X, + chunkRegion.Value.Y, + chunkRegion.Value.Width, + chunkRegion.Value.Height); + } + else + { + CopyFromOrToBuffer( + cbs.CommandBuffer, + buffer, + image, + chunkBufferLength, + false, + chunkLayer, + chunkLevel, + chunkLayers, + chunkLevels, + chunkSingleSlice); + } + } } } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 3b15c1336..cc0c18fa1 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -44,7 +44,7 @@ namespace Ryujinx.HLE MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable ? MemoryAllocationFlags.Reserve - : MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable; + : MemoryAllocationFlags.Reserve; // | MemoryAllocationFlags.Mirrorable; #pragma warning disable IDE0055 // Disable formatting AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver);