diff --git a/source.json b/source.json index 61ecb1bc9..1b6967bb7 100644 --- a/source.json +++ b/source.json @@ -27,7 +27,7 @@ { "version": "1.7.0", "buildVersion": "1", - "date": "2025-04-8", + "date": "2025-04-08", "localizedDescription": "First AltStore release!", "downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa", "size": 79821, @@ -40,7 +40,7 @@ "com.apple.developer.kernel.increased-memory-limit" ], "privacy": { - "NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save images." + "NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save screenshots." } } } diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index 45f0688bf..125cd012b 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Unwinding; using ARMeilleure.Memory; using ARMeilleure.Native; using Ryujinx.Memory; -using Ryujinx.Common.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -16,73 +15,52 @@ namespace ARMeilleure.Translation.Cache static partial class JitCache { private static readonly int _pageSize = (int)MemoryBlock.GetPageSize(); - private static readonly int _pageMask = _pageSize - 4; + private static readonly int _pageMask = _pageSize - 1; private const int CodeAlignment = 4; // Bytes. - private const int CacheSize = 128 * 1024 * 1024; - private const int CacheSizeIOS = 128 * 1024 * 1024; + private const int CacheSize = 2047 * 1024 * 1024; + private const int CacheSizeIOS = 1024 * 1024 * 1024; private static ReservedRegion _jitRegion; private static JitCacheInvalidation _jitCacheInvalidator; - private static List _cacheAllocators = []; + private static CacheMemoryAllocator _cacheAllocator; private static readonly List _cacheEntries = new(); private static readonly object _lock = new(); private static bool _initialized; - private static readonly List _jitRegions = new(); - - private static int _activeRegionIndex = 0; - [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); public static void Initialize(IJitMemoryAllocator allocator) { + if (_initialized) + { + return; + } + lock (_lock) { if (_initialized) { - if (OperatingSystem.IsWindows()) - { - // JitUnwindWindows.RemoveFunctionTableHandler( - // _jitRegions[0].Pointer); - } - - for (int i = 0; i < _jitRegions.Count; i++) - { - _jitRegions[i].Dispose(); - } - - _jitRegions.Clear(); - _cacheAllocators.Clear(); - } - else - { - _initialized = true; + return; } - _activeRegionIndex = 0; - - var firstRegion = new ReservedRegion(allocator, CacheSize); - _jitRegions.Add(firstRegion); - - CacheMemoryAllocator firstCacheAllocator = new(CacheSize); - _cacheAllocators.Add(firstCacheAllocator); + _jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize)); if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) { _jitCacheInvalidator = new JitCacheInvalidation(allocator); } + _cacheAllocator = new CacheMemoryAllocator(CacheSize); + if (OperatingSystem.IsWindows()) { - JitUnwindWindows.InstallFunctionTableHandler( - firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize) - ); + JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize)); } _initialized = true; @@ -95,9 +73,7 @@ namespace ARMeilleure.Translation.Cache { while (_deferredRxProtect.TryDequeue(out var result)) { - ReservedRegion targetRegion = _jitRegions[_activeRegionIndex]; - - ReprotectAsExecutable(targetRegion, result.funcOffset, result.length); + ReprotectAsExecutable(result.funcOffset, result.length); } } @@ -111,8 +87,7 @@ namespace ARMeilleure.Translation.Cache int funcOffset = Allocate(code.Length, deferProtect); - ReservedRegion targetRegion = _jitRegions[_activeRegionIndex]; - IntPtr funcPtr = targetRegion.Pointer + funcOffset; + IntPtr funcPtr = _jitRegion.Pointer + funcOffset; if (OperatingSystem.IsIOS()) { @@ -123,7 +98,8 @@ namespace ARMeilleure.Translation.Cache } else { - ReprotectAsExecutable(targetRegion, funcOffset, code.Length); + ReprotectAsExecutable(funcOffset, code.Length); + JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length); } } @@ -139,9 +115,9 @@ namespace ARMeilleure.Translation.Cache } else { - ReprotectAsWritable(targetRegion, funcOffset, code.Length); + ReprotectAsWritable(funcOffset, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length); - ReprotectAsExecutable(targetRegion, funcOffset, code.Length); + ReprotectAsExecutable(funcOffset, code.Length); if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { @@ -163,50 +139,41 @@ namespace ARMeilleure.Translation.Cache { if (OperatingSystem.IsIOS()) { - // return; + return; } lock (_lock) { - foreach (var region in _jitRegions) + Debug.Assert(_initialized); + + int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); + + if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset) { - if (pointer.ToInt64() < region.Pointer.ToInt64() || - pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64()) - { - continue; - } - - int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64()); - - if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset) - { - _cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size)); - _cacheEntries.RemoveAt(entryIndex); - } - - return; + _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); + _cacheEntries.RemoveAt(entryIndex); } } } - private static void ReprotectAsWritable(ReservedRegion region, int offset, int size) + private static void ReprotectAsWritable(int offset, int size) { int endOffs = offset + size; int regionStart = offset & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask; - region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); + _jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); } - private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size) + private static void ReprotectAsExecutable(int offset, int size) { int endOffs = offset + size; int regionStart = offset & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask; - region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); + _jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); } private static int Allocate(int codeSize, bool deferProtect = false) @@ -220,33 +187,18 @@ namespace ARMeilleure.Translation.Cache alignment = 0x4000; } - int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment); + int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment); - if (allocOffset >= 0) + Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}"); + + if (allocOffset < 0) { - _jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); - return allocOffset; + throw new OutOfMemoryException("JIT Cache exhausted."); } - int exhaustedRegion = _activeRegionIndex; - var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize); - _jitRegions.Add(newRegion); - _activeRegionIndex = _jitRegions.Count - 1; + _jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); - int newRegionNumber = _activeRegionIndex; - - Logger.Info?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize)} Total Allocation)."); - - _cacheAllocators.Add(new CacheMemoryAllocator(CacheSize)); - - int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment); - if (allocOffsetNew < 0) - { - throw new OutOfMemoryException("Failed to allocate in new Cache Region!"); - } - - newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize); - return allocOffsetNew; + return allocOffset; } private static int AlignCodeSize(int codeSize, bool deferProtect = false) @@ -299,4 +251,4 @@ namespace ARMeilleure.Translation.Cache return false; } } -} +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 17c42c37d..5bc597f39 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -724,6 +724,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", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -896,6 +900,16 @@ "$(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/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; @@ -919,7 +933,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = 4D52P7Z7YN; ENABLE_PREVIEWS = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -1000,6 +1014,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", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -1172,9 +1190,19 @@ "$(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/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; + PRODUCT_BUNDLE_IDENTIFIER = "com.stossy11.MeloNX-personal"; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; 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 07a670c83..07441ebda 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/App/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h index 847a0691d..752e409dd 100644 --- a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h +++ b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h @@ -50,7 +50,7 @@ char* installed_firmware_version(); void set_native_window(void *layerPtr); -void stop_emulation(); +void stop_emulation(bool shouldPause); void initialize(); diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift index eea8c81a9..7a634ec2b 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift @@ -371,7 +371,6 @@ class Ryujinx : ObservableObject { self.emulationUIView = nil self.metalLayer = nil - stop_emulation() thread.cancel() } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift index 407f98eb3..89af773c4 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift @@ -10,6 +10,7 @@ import GameController import Darwin import UIKit import MetalKit +import CoreLocation struct MoltenVKSettings: Codable, Hashable { let string: String @@ -159,16 +160,7 @@ struct ContentView: View { 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() - } + ControllerListView(game: $game) )) checkJitStatus() @@ -310,6 +302,8 @@ struct ContentView: View { } private func setupEmulation() { + refreshControllersList() + isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) DispatchQueue.main.async { @@ -327,14 +321,34 @@ struct ContentView: View { controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) }) controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") } - currentControllers = [] - if controllersList.count == 1 { - 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 }) { - currentControllers.append(controller) + if !currentControllers.isEmpty, !(currentControllers.count == 1) { + var currentController: [Controller] = [] + + if currentController.count == 1 { + currentController.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 }) { + currentController.append(controller) + } + } + } + + if currentController == currentControllers { + currentControllers = [] + currentControllers = currentController + } + } else { + currentControllers = [] + + if controllersList.count == 1 { + 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 }) { + currentControllers.append(controller) + } } } } @@ -397,10 +411,12 @@ struct ContentView: View { 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.games.first(where: { $0.titleId == text }) - } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { - game = ryujinx.games.first(where: { $0.titleName == text }) + DispatchQueue.main.async { + if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { + game = ryujinx.games.first(where: { $0.titleId == text }) + } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { + game = ryujinx.games.first(where: { $0.titleName == text }) + } } } } @@ -413,3 +429,136 @@ extension Array { } } } + +class LocationManager: NSObject, CLLocationManagerDelegate { + + private var locationManager: CLLocationManager + + static let sharedInstance = LocationManager() + + private override init() { + locationManager = CLLocationManager() + super.init() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.pausesLocationUpdatesAutomatically = false + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // print("wow") + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Location manager failed with: \(error)") + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + if manager.authorizationStatus == .denied { + print("Location services are disabled in settings.") + } else { + startUpdatingLocation() + } + } + + func stop() { + if UserDefaults.standard.bool(forKey: "location-enabled") { + locationManager.stopUpdatingLocation() + } + } + + func startUpdatingLocation() { + if UserDefaults.standard.bool(forKey: "location-enabled") { + locationManager.requestAlwaysAuthorization() + locationManager.allowsBackgroundLocationUpdates = true + locationManager.startUpdatingLocation() + } + } +} + + +struct ControllerListView: View { + @State private var selectedIndex = 0 + @Binding var game: Game? + @ObservedObject private var ryujinx = Ryujinx.shared + + var body: some View { + List(ryujinx.games.indices, id: \.self) { index in + let game = ryujinx.games[index] + + HStack(spacing: 16) { + // Game Icon + Group { + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + } else { + ZStack { + RoundedRectangle(cornerRadius: 10) + Image(systemName: "gamecontroller.fill") + .font(.system(size: 24)) + .foregroundColor(.gray) + } + } + } + .frame(width: 55, height: 55) + .cornerRadius(10) + + // Game Info + VStack(alignment: .leading, spacing: 4) { + Text(game.titleName) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.primary) + + HStack(spacing: 4) { + Text(game.developer) + + if !game.version.isEmpty && game.version != "0" { + Text("•") + Text("v\(game.version)") + } + } + .font(.system(size: 14)) + .foregroundColor(.secondary) + } + + Spacer() + } + .background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear) + } + .onAppear(perform: setupControllerObservers) + } + + private func setupControllerObservers() { + let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in + if yValue == 1.0 { + selectedIndex = max(0, selectedIndex - 1) + } else if yValue == -1.0 { + selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1) + } + } + + for controller in GCController.controllers() { + print("Controller connected: \(controller.vendorName ?? "Unknown")") + controller.playerIndex = .index1 + + controller.microGamepad?.dpad.valueChangedHandler = dpadHandler + controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler + + controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in + if pressed { + print("A button pressed") + game = ryujinx.games[selectedIndex] + } + } + } + + NotificationCenter.default.addObserver( + forName: .GCControllerDidConnect, + object: nil, + queue: .main + ) { _ in + setupControllerObservers() + } + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift index e3166b147..3114ad459 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift @@ -19,6 +19,9 @@ struct EmulationView: View { @Binding var startgame: Game? @Environment(\.scenePhase) var scenePhase + @State private var isInBackground = false + @AppStorage("location-enabled") var locationenabled: Bool = false + var body: some View { ZStack { if isAirplaying { @@ -26,7 +29,7 @@ struct EmulationView: View { .ignoresSafeArea() .edgesIgnoringSafeArea(.all) .onAppear { - Air.play(AnyView(MetalView().ignoresSafeArea())) + Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all))) } } else { MetalView() // The Emulation View @@ -88,6 +91,7 @@ struct EmulationView: View { } } .onAppear { + LocationManager.sharedInstance.startUpdatingLocation() Air.shared.connectionCallbacks.append { cool in DispatchQueue.main.async { isAirplaying = cool @@ -95,5 +99,18 @@ struct EmulationView: View { } } } + .onChange(of: scenePhase) { newPhase in + // Detect when the app enters the background + if newPhase == .background { + stop_emulation(true) + isInBackground = true + } else if newPhase == .active { + stop_emulation(false) + isInBackground = false + } else if newPhase == .inactive { + stop_emulation(true) + isInBackground = true + } + } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift index 4be5c0d22..967acf901 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift @@ -429,61 +429,16 @@ struct SettingsView: View { Text("Controller Selection") .font(.headline) .foregroundColor(.primary) - Divider() - if !currentControllers.isEmpty { - ForEach(currentControllers) { controller in - if currentControllers.firstIndex(of: controller) == currentControllers.count - 1 && currentControllers.count != 1 { - Divider() - } - - HStack { - Image(systemName: "gamecontroller.fill") - .foregroundColor(.blue) - - if let index = currentControllers.firstIndex(where: { $0.id == controller.id }) { - Text("Player \(index + 1): \(controller.name)") - .lineLimit(1) - } - - Spacer() - - Button { - toggleController(controller) - } label: { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.secondary) - } - .buttonStyle(.plain) - } - - if currentControllers[0] != controller && currentControllers.firstIndex(of: controller) == currentControllers.count - 1 { - Divider() - } - } - .onMove { from, to in - currentControllers.move(fromOffsets: from, toOffset: to) - } - .environment(\.editMode, .constant(.active)) + if currentControllers.isEmpty { + emptyControllersView + } else { + controllerListView } - if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty { + if hasAvailableControllers { Divider() - - Menu { - ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in - Button { - currentControllers.append(controller) - } label: { - Text(controller.name) - } - } - } label: { - Label("Add Controller", systemImage: "plus.circle.fill") - .foregroundColor(.blue) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 6) - } + addControllerButton } } } @@ -556,6 +511,78 @@ struct SettingsView: View { } } } + + // MARK: - Controller Selection Components + + private var hasAvailableControllers: Bool { + !controllersList.filter { !currentControllers.contains($0) }.isEmpty + } + + private var emptyControllersView: some View { + HStack { + Text("No controllers selected (Keyboard will be used)") + .foregroundColor(.secondary) + .italic() + Spacer() + } + .padding(.vertical, 8) + } + + private var controllerListView: some View { + VStack(spacing: 0) { + Divider() + + ForEach(currentControllers.indices, id: \.self) { index in + let controller = currentControllers[index] + + VStack(spacing: 0) { + HStack { + Image(systemName: "gamecontroller.fill") + .foregroundColor(.blue) + + Text("Player \(index + 1): \(controller.name)") + .lineLimit(1) + + Spacer() + + Button { + toggleController(controller) + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + .padding(.vertical, 8) + + if index < currentControllers.count - 1 { + Divider() + } + } + } + .onMove { from, to in + currentControllers.move(fromOffsets: from, toOffset: to) + } + .environment(\.editMode, .constant(.active)) + } + } + + private var addControllerButton: some View { + Menu { + ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in + Button { + currentControllers.append(controller) + } label: { + Text(controller.name) + } + } + } label: { + Label("Add Controller", systemImage: "plus.circle.fill") + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 6) + } + } // MARK: - System Settings diff --git a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift index c800312b2..708786b48 100644 --- a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift +++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift @@ -9,6 +9,8 @@ import SwiftUI import UIKit import CryptoKit import UniformTypeIdentifiers +import AVFoundation + extension UIDocumentPickerViewController { @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController { @@ -29,6 +31,8 @@ struct MeloNXApp: App { @State var finished = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false + @AppStorage("location-enabled") var locationenabled: Bool = false + var body: some Scene { WindowGroup { if finishedStorage { diff --git a/src/MeloNX/MeloNX/Info.plist b/src/MeloNX/MeloNX/Info.plist index bb9bd4980..8c026ae5d 100644 --- a/src/MeloNX/MeloNX/Info.plist +++ b/src/MeloNX/MeloNX/Info.plist @@ -38,8 +38,9 @@ UIBackgroundModes - audio + location processing + audio UIFileSharingEnabled diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 0387d40f0..bf649541b 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -403,10 +403,22 @@ namespace Ryujinx.Headless.SDL2 } [UnmanagedCallersOnly(EntryPoint = "stop_emulation")] - public static void StopEmulation() + public static void StopEmulation(bool shouldPause) { if (_window != null) { + if (!shouldPause) + { + _window.Device.SetVolume(1); + _window._isPaused = false; + _window._pauseEvent.Set(); + } + else + { + _window.Device.SetVolume(0); + _window._isPaused = true; + _window._pauseEvent.Reset(); + } } } @@ -886,19 +898,9 @@ namespace Ryujinx.Headless.SDL2 { if (inputId == null) { - if (index == PlayerIndex.Player1) - { - Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard."); + Logger.Info?.Print(LogClass.Application, $"{index} not configured"); - // Default to keyboard - inputId = "0"; - } - else - { - Logger.Info?.Print(LogClass.Application, $"{index} not configured"); - - return null; - } + return null; } IGamepad gamepad; @@ -989,12 +991,26 @@ namespace Ryujinx.Headless.SDL2 { bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons"); + ControllerType currentController; + if (index == PlayerIndex.Handheld) + { + currentController = ControllerType.Handheld; + } + else if (gamepadName.Contains("Joycons") || gamepadName.Contains("Backbone")) + { + currentController = ControllerType.JoyconPair; + } + else + { + currentController = ControllerType.ProController; + } + config = new StandardControllerInputConfig { Version = InputConfig.CurrentVersion, Backend = InputBackendType.GamepadSDL2, Id = null, - ControllerType = ControllerType.JoyconPair, + ControllerType = currentController, DeadzoneLeft = 0.1f, DeadzoneRight = 0.1f, RangeLeft = 1.0f, diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index 5f8eda72a..77896817a 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -44,6 +44,9 @@ namespace Ryujinx.Headless.SDL2 _mainThreadActions.Enqueue(action); } + public bool _isPaused; + public ManualResetEvent _pauseEvent; + public NpadManager NpadManager; public TouchScreenManager TouchScreenManager; public Switch Device; @@ -104,6 +107,7 @@ namespace Ryujinx.Headless.SDL2 _gpuCancellationTokenSource = new CancellationTokenSource(); _exitEvent = new ManualResetEvent(false); _gpuDoneEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); _aspectRatio = aspectRatio; _enableMouse = enableMouse; HostUITheme = new HeadlessHostUiTheme(); @@ -298,6 +302,8 @@ namespace Ryujinx.Headless.SDL2 return; } + _pauseEvent.WaitOne(); + _ticks += _chrono.ElapsedTicks; _chrono.Restart(); @@ -378,7 +384,6 @@ namespace Ryujinx.Headless.SDL2 { while (_isActive) { - UpdateFrame(); SDL_PumpEvents();