0
0
mirror of https://git.743378673.xyz/MeloNX/MeloNX.git synced 2025-04-24 08:25:14 +00:00

Add Fixed Handheld mode, Location to keep game running in the background, New Airplay Menu amd more

This commit is contained in:
Stossy11 2025-04-10 22:30:56 +10:00
parent 15171a703a
commit e382a35387
13 changed files with 381 additions and 183 deletions

View File

@ -27,7 +27,7 @@
{ {
"version": "1.7.0", "version": "1.7.0",
"buildVersion": "1", "buildVersion": "1",
"date": "2025-04-8", "date": "2025-04-08",
"localizedDescription": "First AltStore release!", "localizedDescription": "First AltStore release!",
"downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa", "downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
"size": 79821, "size": 79821,
@ -40,7 +40,7 @@
"com.apple.developer.kernel.increased-memory-limit" "com.apple.developer.kernel.increased-memory-limit"
], ],
"privacy": { "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."
} }
} }
} }

View File

@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Native; using ARMeilleure.Native;
using Ryujinx.Memory; using Ryujinx.Memory;
using Ryujinx.Common.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -16,73 +15,52 @@ namespace ARMeilleure.Translation.Cache
static partial class JitCache static partial class JitCache
{ {
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize(); 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 CodeAlignment = 4; // Bytes.
private const int CacheSize = 128 * 1024 * 1024; private const int CacheSize = 2047 * 1024 * 1024;
private const int CacheSizeIOS = 128 * 1024 * 1024; private const int CacheSizeIOS = 1024 * 1024 * 1024;
private static ReservedRegion _jitRegion; private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
private static List<CacheMemoryAllocator> _cacheAllocators = []; private static CacheMemoryAllocator _cacheAllocator;
private static readonly List<CacheEntry> _cacheEntries = new(); private static readonly List<CacheEntry> _cacheEntries = new();
private static readonly object _lock = new(); private static readonly object _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
public static void Initialize(IJitMemoryAllocator allocator) public static void Initialize(IJitMemoryAllocator allocator)
{ {
if (_initialized)
{
return;
}
lock (_lock) lock (_lock)
{ {
if (_initialized) if (_initialized)
{ {
if (OperatingSystem.IsWindows()) return;
{
// JitUnwindWindows.RemoveFunctionTableHandler(
// _jitRegions[0].Pointer);
}
for (int i = 0; i < _jitRegions.Count; i++)
{
_jitRegions[i].Dispose();
}
_jitRegions.Clear();
_cacheAllocators.Clear();
}
else
{
_initialized = true;
} }
_activeRegionIndex = 0; _jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
_cacheAllocators.Add(firstCacheAllocator);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{ {
_jitCacheInvalidator = new JitCacheInvalidation(allocator); _jitCacheInvalidator = new JitCacheInvalidation(allocator);
} }
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
JitUnwindWindows.InstallFunctionTableHandler( JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
} }
_initialized = true; _initialized = true;
@ -95,9 +73,7 @@ namespace ARMeilleure.Translation.Cache
{ {
while (_deferredRxProtect.TryDequeue(out var result)) while (_deferredRxProtect.TryDequeue(out var result))
{ {
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex]; ReprotectAsExecutable(result.funcOffset, result.length);
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
} }
} }
@ -111,8 +87,7 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = Allocate(code.Length, deferProtect); int funcOffset = Allocate(code.Length, deferProtect);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex]; IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
@ -123,7 +98,8 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsExecutable(targetRegion, funcOffset, code.Length); ReprotectAsExecutable(funcOffset, code.Length);
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length); JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
} }
} }
@ -139,9 +115,9 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsWritable(targetRegion, funcOffset, code.Length); ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, 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) if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -163,50 +139,41 @@ namespace ARMeilleure.Translation.Cache
{ {
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
// return; return;
} }
lock (_lock) 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() || _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64()) _cacheEntries.RemoveAt(entryIndex);
{
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;
} }
} }
} }
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size) private static void ReprotectAsWritable(int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_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 endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_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) private static int Allocate(int codeSize, bool deferProtect = false)
@ -220,33 +187,18 @@ namespace ARMeilleure.Translation.Cache
alignment = 0x4000; 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); throw new OutOfMemoryException("JIT Cache exhausted.");
return allocOffset;
} }
int exhaustedRegion = _activeRegionIndex; _jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex; return allocOffset;
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;
} }
private static int AlignCodeSize(int codeSize, bool deferProtect = false) private static int AlignCodeSize(int codeSize, bool deferProtect = false)
@ -299,4 +251,4 @@ namespace ARMeilleure.Translation.Cache
return false; return false;
} }
} }
} }

View File

@ -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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@ -919,7 +933,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = 4D52P7Z7YN;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = ( 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = "com.stossy11.MeloNX-personal";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;

View File

@ -50,7 +50,7 @@ char* installed_firmware_version();
void set_native_window(void *layerPtr); void set_native_window(void *layerPtr);
void stop_emulation(); void stop_emulation(bool shouldPause);
void initialize(); void initialize();

View File

@ -371,7 +371,6 @@ class Ryujinx : ObservableObject {
self.emulationUIView = nil self.emulationUIView = nil
self.metalLayer = nil self.metalLayer = nil
stop_emulation()
thread.cancel() thread.cancel()
} }

View File

@ -10,6 +10,7 @@ import GameController
import Darwin import Darwin
import UIKit import UIKit
import MetalKit import MetalKit
import CoreLocation
struct MoltenVKSettings: Codable, Hashable { struct MoltenVKSettings: Codable, Hashable {
let string: String let string: String
@ -159,16 +160,7 @@ struct ContentView: View {
initControllerObservers() initControllerObservers()
Air.play(AnyView( Air.play(AnyView(
VStack { ControllerListView(game: $game)
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
)) ))
checkJitStatus() checkJitStatus()
@ -310,6 +302,8 @@ struct ContentView: View {
} }
private func setupEmulation() { private func setupEmulation() {
refreshControllersList()
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
DispatchQueue.main.async { DispatchQueue.main.async {
@ -327,14 +321,34 @@ struct ContentView: View {
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) }) controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") } controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = []
if controllersList.count == 1 { if !currentControllers.isEmpty, !(currentControllers.count == 1) {
currentControllers.append(controllersList[0]) var currentController: [Controller] = []
} else if (controllersList.count - 1) >= 1 {
for controller in controllersList { if currentController.count == 1 {
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) { currentController.append(controllersList[0])
currentControllers.append(controller) } 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) { private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" { components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { DispatchQueue.main.async {
game = ryujinx.games.first(where: { $0.titleId == text }) if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { game = ryujinx.games.first(where: { $0.titleId == text })
game = ryujinx.games.first(where: { $0.titleName == 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()
}
}
}

View File

@ -19,6 +19,9 @@ struct EmulationView: View {
@Binding var startgame: Game? @Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View { var body: some View {
ZStack { ZStack {
if isAirplaying { if isAirplaying {
@ -26,7 +29,7 @@ struct EmulationView: View {
.ignoresSafeArea() .ignoresSafeArea()
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.onAppear { .onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea())) Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
} }
} else { } else {
MetalView() // The Emulation View MetalView() // The Emulation View
@ -88,6 +91,7 @@ struct EmulationView: View {
} }
} }
.onAppear { .onAppear {
LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async { DispatchQueue.main.async {
isAirplaying = cool 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
}
}
} }
} }

View File

@ -429,61 +429,16 @@ struct SettingsView: View {
Text("Controller Selection") Text("Controller Selection")
.font(.headline) .font(.headline)
.foregroundColor(.primary) .foregroundColor(.primary)
Divider()
if !currentControllers.isEmpty { if currentControllers.isEmpty {
ForEach(currentControllers) { controller in emptyControllersView
if currentControllers.firstIndex(of: controller) == currentControllers.count - 1 && currentControllers.count != 1 { } else {
Divider() controllerListView
}
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 !controllersList.filter({ !currentControllers.contains($0) }).isEmpty { if hasAvailableControllers {
Divider() Divider()
addControllerButton
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)
}
} }
} }
} }
@ -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 // MARK: - System Settings

View File

@ -9,6 +9,8 @@ import SwiftUI
import UIKit import UIKit
import CryptoKit import CryptoKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
import AVFoundation
extension UIDocumentPickerViewController { extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController { @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
@ -29,6 +31,8 @@ struct MeloNXApp: App {
@State var finished = false @State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
if finishedStorage { if finishedStorage {

View File

@ -38,8 +38,9 @@
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>location</string>
<string>processing</string> <string>processing</string>
<string>audio</string>
</array> </array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>

View File

@ -403,10 +403,22 @@ namespace Ryujinx.Headless.SDL2
} }
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")] [UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
public static void StopEmulation() public static void StopEmulation(bool shouldPause)
{ {
if (_window != null) 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 (inputId == null)
{ {
if (index == PlayerIndex.Player1) Logger.Info?.Print(LogClass.Application, $"{index} not configured");
{
Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
// Default to keyboard return null;
inputId = "0";
}
else
{
Logger.Info?.Print(LogClass.Application, $"{index} not configured");
return null;
}
} }
IGamepad gamepad; IGamepad gamepad;
@ -989,12 +991,26 @@ namespace Ryujinx.Headless.SDL2
{ {
bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons"); 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 config = new StandardControllerInputConfig
{ {
Version = InputConfig.CurrentVersion, Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2, Backend = InputBackendType.GamepadSDL2,
Id = null, Id = null,
ControllerType = ControllerType.JoyconPair, ControllerType = currentController,
DeadzoneLeft = 0.1f, DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f, DeadzoneRight = 0.1f,
RangeLeft = 1.0f, RangeLeft = 1.0f,

View File

@ -44,6 +44,9 @@ namespace Ryujinx.Headless.SDL2
_mainThreadActions.Enqueue(action); _mainThreadActions.Enqueue(action);
} }
public bool _isPaused;
public ManualResetEvent _pauseEvent;
public NpadManager NpadManager; public NpadManager NpadManager;
public TouchScreenManager TouchScreenManager; public TouchScreenManager TouchScreenManager;
public Switch Device; public Switch Device;
@ -104,6 +107,7 @@ namespace Ryujinx.Headless.SDL2
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false); _exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false); _gpuDoneEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
_aspectRatio = aspectRatio; _aspectRatio = aspectRatio;
_enableMouse = enableMouse; _enableMouse = enableMouse;
HostUITheme = new HeadlessHostUiTheme(); HostUITheme = new HeadlessHostUiTheme();
@ -298,6 +302,8 @@ namespace Ryujinx.Headless.SDL2
return; return;
} }
_pauseEvent.WaitOne();
_ticks += _chrono.ElapsedTicks; _ticks += _chrono.ElapsedTicks;
_chrono.Restart(); _chrono.Restart();
@ -378,7 +384,6 @@ namespace Ryujinx.Headless.SDL2
{ {
while (_isActive) while (_isActive)
{ {
UpdateFrame(); UpdateFrame();
SDL_PumpEvents(); SDL_PumpEvents();