diff --git a/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs b/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs index 639e4476b..a736733df 100644 --- a/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs +++ b/src/ARMeilleure/CodeGen/Arm64/HardwareCapabilities.cs @@ -19,8 +19,7 @@ namespace ARMeilleure.CodeGen.Arm64 LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP); LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2); } - - if (OperatingSystem.IsMacOS()) + if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) { for (int i = 0; i < _sysctlNames.Length; i++) { @@ -130,6 +129,7 @@ namespace ARMeilleure.CodeGen.Arm64 private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, nint newValue, ulong newValueSize); [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] private static bool CheckSysctlName(string name) { ulong size = sizeof(int); diff --git a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs index 35747d7a4..0e0992609 100644 --- a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs +++ b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs @@ -87,13 +87,13 @@ namespace ARMeilleure.Signal private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr) { - ulong structAddressOffset = OperatingSystem.IsMacOS() ? 24ul : 16ul; // si_addr + ulong structAddressOffset = (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) ? 24ul : 16ul; // si_addr return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset))); } private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr) { - if (OperatingSystem.IsMacOS()) + if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) { const ulong McontextOffset = 48; // uc_mcontext Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset))); diff --git a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs index f36bf7a3d..a1bd3933a 100644 --- a/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs +++ b/src/ARMeilleure/Translation/Cache/CacheMemoryAllocator.cs @@ -30,21 +30,26 @@ namespace ARMeilleure.Translation.Cache _blocks.Add(new MemoryBlock(0, capacity)); } - public int Allocate(int size) + public int Allocate(ref int size, int alignment) { + int alignM1 = alignment - 1; for (int i = 0; i < _blocks.Count; i++) { MemoryBlock block = _blocks[i]; + int misAlignment = ((block.Offset + alignM1) & (~alignM1)) - block.Offset; + int alignedSize = size + misAlignment; - if (block.Size > size) + if (block.Size > alignedSize) { - _blocks[i] = new MemoryBlock(block.Offset + size, block.Size - size); - return block.Offset; + size = alignedSize; + _blocks[i] = new MemoryBlock(block.Offset + alignedSize, block.Size - alignedSize); + return block.Offset + misAlignment; } - else if (block.Size == size) + else if (block.Size == alignedSize) { + size = alignedSize; _blocks.RemoveAt(i); - return block.Offset; + return block.Offset + misAlignment; } } diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index 6515b89c4..ae2e30c0d 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -47,7 +47,7 @@ namespace ARMeilleure.Translation.Cache return; } - _jitRegion = new ReservedRegion(allocator, CacheSize); + _jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize)); if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) { @@ -80,6 +80,7 @@ namespace ARMeilleure.Translation.Cache if (OperatingSystem.IsIOS()) { + ReprotectAsWritable(funcOffset, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length); ReprotectAsExecutable(funcOffset, code.Length); @@ -119,6 +120,13 @@ namespace ARMeilleure.Translation.Cache public static void Unmap(nint pointer) { + + if (OperatingSystem.IsIOS()) + { + return; + } + + lock (_lock) { Debug.Assert(_initialized); @@ -157,7 +165,18 @@ namespace ARMeilleure.Translation.Cache { codeSize = AlignCodeSize(codeSize); - int allocOffset = _cacheAllocator.Allocate(codeSize); + int alignment = CodeAlignment; + + if (OperatingSystem.IsIOS()) + { + alignment = 0x4000; + } + + int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment); + + //DEBUG: Show JIT Memory Allocation + + //Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}"); if (allocOffset < 0) { @@ -171,6 +190,13 @@ namespace ARMeilleure.Translation.Cache private static int AlignCodeSize(int codeSize) { + int alignment = CodeAlignment; + + if (OperatingSystem.IsIOS()) + { + alignment = 0x4000; + } + return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); } diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 841e5fefa..315efb634 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -1022,6 +1022,7 @@ namespace ARMeilleure.Translation.PTC osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1; osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2; osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3; + osPlatform |= (OperatingSystem.IsIOS() ? 1u : 0u) << 4; #pragma warning restore IDE0055 return osPlatform; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 9c95d1966..d291e1153 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -62,7 +62,6 @@ 4E6715F12CFEEB6E00425F0C /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = { isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; attributesByRelativePath = { - "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libavcodec.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, ); @@ -82,7 +81,6 @@ "Dependencies/Dynamic Libraries/libavcodec.dylib", "Dependencies/Dynamic Libraries/libavutil.dylib", "Dependencies/Dynamic Libraries/libMoltenVK.dylib", - "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib", Dependencies/XCFrameworks/libavcodec.xcframework, Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavformat.xcframework, @@ -579,6 +577,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 = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; @@ -694,6 +702,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 = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; diff --git a/src/MeloNX/MeloNX/Core/DetectJIT/utils.h b/src/MeloNX/MeloNX/Core/DetectJIT/utils.h new file mode 100644 index 000000000..380d18050 --- /dev/null +++ b/src/MeloNX/MeloNX/Core/DetectJIT/utils.h @@ -0,0 +1,27 @@ +#if __has_feature(modules) +@import UIKit; +@import Foundation; +#else +#import "UIKit/UIKit.h" +#import "Foundation/Foundation.h" +#endif + +#define DISPATCH_ASYNC_START dispatch_async(dispatch_get_main_queue(), ^{ +#define DISPATCH_ASYNC_CLOSE }); + +#define PT_TRACE_ME 0 +extern int ptrace(int, pid_t, caddr_t, int); + +#define CS_DEBUGGED 0x10000000 +extern int csops( + pid_t pid, + unsigned int ops, + void *useraddr, + size_t usersize + ); + +extern BOOL getEntitlementValue(NSString *key); +extern BOOL isJITEnabled(void); + +#define DLOG(format, ...) ShowAlert(@"DEBUG", [NSString stringWithFormat:@"\n %s [Line %d] \n %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]]) +void ShowAlert(NSString* title, NSString* message, _Bool* showok); diff --git a/src/MeloNX/MeloNX/Core/DetectJIT/utils.m b/src/MeloNX/MeloNX/Core/DetectJIT/utils.m new file mode 100644 index 000000000..664ad43f1 --- /dev/null +++ b/src/MeloNX/MeloNX/Core/DetectJIT/utils.m @@ -0,0 +1,91 @@ +#import "utils.h" + +typedef struct __SecTask * SecTaskRef; +extern CFTypeRef SecTaskCopyValueForEntitlement( + SecTaskRef task, + NSString* entitlement, + CFErrorRef _Nullable *error + ) + __attribute__((weak_import)); + +extern SecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator) + __attribute__((weak_import)); + +BOOL getEntitlementValue(NSString *key) +{ + if (SecTaskCreateFromSelf == NULL || SecTaskCopyValueForEntitlement == NULL) + return NO; + SecTaskRef sec_task = SecTaskCreateFromSelf(NULL); + if(!sec_task) return NO; + CFTypeRef value = SecTaskCopyValueForEntitlement(sec_task, key, nil); + if (value != nil) + { + CFRelease(value); + } + CFRelease(sec_task); + return value != nil && [(__bridge id)value boolValue]; +} + +BOOL isJITEnabled(void) +{ + if (getEntitlementValue(@"dynamic-codesigning")) + { + return YES; + } + + int flags; + csops(getpid(), 0, &flags, sizeof(flags)); + return (flags & CS_DEBUGGED) != 0; +} + +void ShowAlert(NSString* title, NSString* message, _Bool* showok) +{ + DISPATCH_ASYNC_START + UIWindow* mainWindow = [[UIApplication sharedApplication] windows].lastObject; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + if (showok) { + [alert addAction:[UIAlertAction actionWithTitle:@"ok!" + style:UIAlertActionStyleDefault + handler:nil]]; + } + [mainWindow.rootViewController presentViewController:alert + animated:true + completion:nil]; + DISPATCH_ASYNC_CLOSE +} + +#import <UIKit/UIKit.h> + +__attribute__((constructor)) static void entry(int argc, char **argv) +{ + if (isJITEnabled()) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:@"JIT"]; + [defaults synchronize]; // Ensure the value is saved immediately + } else { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:NO forKey:@"JIT"]; + [defaults synchronize]; // Ensure the value is saved immediately + } + + if (getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) { + NSLog(@"Entitlement Does Exist"); + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:@"increased-memory-limit"]; + [defaults synchronize]; // Ensure the value is saved immediately + } + + if (getEntitlementValue(@"com.apple.developer.kernel.increased-debugging-memory-limit")) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:@"increased-debugging-memory-limit"]; + [defaults synchronize]; // Ensure the value is saved immediately + } + if (getEntitlementValue(@"com.apple.developer.kernel.extended-virtual-addressing")) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:YES forKey:@"extended-virtual-addressing"]; + [defaults synchronize]; // Ensure the value is saved immediately + } + +} diff --git a/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h index 89a1b6329..12cba91e5 100644 --- a/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h +++ b/src/MeloNX/MeloNX/Core/Headers/Ryujinx-Header.h @@ -8,13 +8,29 @@ #ifndef RyujinxHeader #define RyujinxHeader + +#import "SDL2/SDL.h" + #ifdef __cplusplus extern "C" { #endif +struct GameInfo { + long FileSize; + char TitleName[512]; + long TitleId; + char Developer[256]; + int Version; + unsigned char* ImageData; + unsigned int ImageSize; +}; + +// extern struct GameInfo get_game_info(int, char*); // Declare the main_ryujinx_sdl function, matching the signature int main_ryujinx_sdl(int argc, char **argv); +// void initialize(); + const char* get_game_controllers(); #ifdef __cplusplus diff --git a/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift b/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift index d8bc92770..e89e9f6f4 100644 --- a/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift +++ b/src/MeloNX/MeloNX/Core/MetalHUD/MTLHUD.swift @@ -7,6 +7,7 @@ import Foundation + class MTLHud { var canMetalHud: Bool { diff --git a/src/MeloNX/MeloNX/Core/Swift/Controller/VirtualController.swift b/src/MeloNX/MeloNX/Core/Swift/Controller/VirtualController.swift index e55b51eab..11415e6d0 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Controller/VirtualController.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Controller/VirtualController.swift @@ -2,98 +2,188 @@ // VirtualController.swift // MeloNX // -// Created by Stossy11 on 28/11/2024. +// Created by Stossy11 on 8/12/2024. // import Foundation -import GameController +import CoreHaptics import UIKit -public var controllerCallback: (() -> Void)? - -var VirtualController: GCVirtualController! -func showVirtualController() { - let config = GCVirtualController.Configuration() - if UserDefaults.standard.bool(forKey: "RyuDemoControls") { - config.elements = [ - GCInputLeftThumbstick, - GCInputButtonA, - GCInputButtonB, - GCInputButtonX, - GCInputButtonY, - // GCInputRightThumbstick, - GCInputRightTrigger, - GCInputLeftTrigger, - GCInputLeftShoulder, - GCInputRightShoulder - ] - } else { - config.elements = [ - GCInputLeftThumbstick, - GCInputButtonA, - GCInputButtonB, - GCInputButtonX, - GCInputButtonY, - GCInputRightThumbstick, - GCInputRightTrigger, - GCInputLeftTrigger, - GCInputLeftShoulder, - GCInputRightShoulder - ] +class VirtualController { + private var instanceID: SDL_JoystickID = -1 + private var controller: OpaquePointer? + + public let controllername = "MeloNX Touch Controller" + + init() { + setupVirtualController() } - VirtualController = GCVirtualController(configuration: config) - VirtualController.connect { err in - print("controller connect: \(String(describing: err))") - patchMakeKeyAndVisible() - if let controllerCallback { - controllerCallback() + + private func setupVirtualController() { + // Initialize SDL if not already initialized + if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 { + SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER)) } - } -} - -func waitforcontroller() { - Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in - if let window = UIApplication.shared.windows.first { - // Function to recursively search for GCControllerView - func findGCControllerView(in view: UIView) -> UIView? { - // Check if current view is GCControllerView - if String(describing: type(of: view)) == "GCControllerView" { - return view + // Create virtual controller + var joystickDesc = SDL_VirtualJoystickDesc( + version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION), + type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), + naxes: 6, + nbuttons: 15, + nhats: 1, + vendor_id: 0, + product_id: 0, + padding: 0, + button_mask: 0, + axis_mask: 0, + name: controllername.withCString { $0 }, + userdata: nil, + Update: { userdata in + // Update joystick state here + }, + SetPlayerIndex: { userdata, playerIndex in + print("Player index set to \(playerIndex)") + }, + Rumble: { userdata, lowFreq, highFreq in + print("Rumble with \(lowFreq), \(highFreq)") + VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) + return 0 + }, + RumbleTriggers: { userdata, leftRumble, rightRumble in + print("Trigger rumble with \(leftRumble), \(rightRumble)") + return 0 + }, + SetLED: { userdata, red, green, blue in + print("Set LED to RGB(\(red), \(green), \(blue))") + return 0 + }, + SendEffect: { userdata, data, size in + print("Effect sent with size \(size)") + return 0 } - - // Search through subviews - for subview in view.subviews { - if let found = findGCControllerView(in: subview) { - return found - } - } - - return nil - } - - if let gcControllerView = findGCControllerView(in: window) { - // Found the GCControllerView - print("Found GCControllerView:", gcControllerView) - - if let theWindow = theWindow, (findGCControllerView(in: theWindow) == nil) { - theWindow.addSubview(gcControllerView) - - theWindow.bringSubviewToFront(gcControllerView) - } - } + ) + + instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) + if instanceID < 0 { + print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") + return } + + // Open a game controller for the virtual joystick + let joystick = SDL_JoystickFromInstanceID(instanceID) + controller = SDL_GameControllerOpen(Int32(instanceID)) + + if controller == nil { + print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") + return + } + } + + static func rumble(lowFreq: Float, highFreq: Float) { + do { + // Low-frequency haptic pattern + let lowFreqPattern = try CHHapticPattern(events: [ + CHHapticEvent(eventType: .hapticTransient, parameters: [ + CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) + ], relativeTime: 0, duration: 0.2) + ], parameters: []) + + // High-frequency haptic pattern + let highFreqPattern = try CHHapticPattern(events: [ + CHHapticEvent(eventType: .hapticTransient, parameters: [ + CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) + ], relativeTime: 0.2, duration: 0.2) + ], parameters: []) + + // Create and start the haptic engine + let engine = try CHHapticEngine() + try engine.start() + + // Create and play the low-frequency player + let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) + try lowFreqPlayer.start(atTime: 0) + + // Create and play the high-frequency player after a short delay + let highFreqPlayer = try engine.makePlayer(with: highFreqPattern) + try highFreqPlayer.start(atTime: 0.2) + + } catch { + print("Error creating haptic patterns: \(error)") + } + } + + + func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { + guard controller != nil else { return } + let joystick = SDL_JoystickFromInstanceID(instanceID) + SDL_JoystickSetVirtualAxis(joystick, axis.rawValue, value) + } + + func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) { + let scaleFactor = 32767.0 / 160 + + let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor))) + let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor))) + + if stick == .right { + updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue)) + updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue)) + } else { // ThumbstickType.left + updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue)) + updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue)) + } + } + + func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { + guard controller != nil else { return } + + print("Button: \(button.rawValue) {state: \(state)}") + if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) { + let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT + let value: Int = (state == 1) ? 32767 : 0 + updateAxisValue(value: Sint16(value), forAxis: axis) + } else { + let joystick = SDL_JoystickFromInstanceID(instanceID) + SDL_JoystickSetVirtualButton(joystick, Int32(button.rawValue), state) + } + } + + func cleanup() { + if let controller = controller { + SDL_GameControllerClose(controller) + self.controller = nil + } + } + + deinit { + cleanup() } } -@available(iOS 15.0, *) -func reconnectVirtualController() { - VirtualController.disconnect() - DispatchQueue.main.async { - VirtualController.connect { err in - print("reconnected: err \(String(describing: err))") - } - } +enum VirtualControllerButton: Int { + case B + case A + case Y + case X + case back + case guide + case start + case leftStick + case rightStick + case leftShoulder + case rightShoulder + case dPadUp + case dPadDown + case dPadLeft + case dPadRight + case leftTrigger + case rightTrigger } - +enum ThumbstickType: Int { + case left + case right +} diff --git a/src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift b/src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift new file mode 100644 index 000000000..43cf638f7 --- /dev/null +++ b/src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift @@ -0,0 +1,65 @@ +// +// VirtualController.swift +// MeloNX +// +// Created by Stossy11 on 28/11/2024. +// + +import Foundation +import GameController +import UIKit +import SwiftUI + +func waitforcontroller() { + if let window = theWindow { + + + + // Function to recursively search for GCControllerView + func findGCControllerView(in view: UIView) -> UIView? { + // Check if current view is GCControllerView + if String(describing: type(of: view)) == "ControllerView" { + return view + } + + // Search through subviews + for subview in view.subviews { + if let found = findGCControllerView(in: subview) { + return found + } + } + + return nil + } + + let controllerView = ControllerView() + let controllerHostingController = UIHostingController(rootView: controllerView) + let containerView = TransparentHostingContainerView(frame: window.bounds) + containerView.backgroundColor = .clear + + controllerHostingController.view.frame = containerView.bounds + controllerHostingController.view.backgroundColor = .clear + containerView.addSubview(controllerHostingController.view) + + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in + if findGCControllerView(in: window) == nil { + window.addSubview(containerView) + + } + + window.bringSubviewToFront(containerView) + } + + } +} + + +class TransparentHostingContainerView: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // Check if the point is within the subviews of this container + let view = super.hitTest(point, with: event) + + // Return nil if the touch is outside visible content (passes through to views below) + return view === self ? nil : view + } +} diff --git a/src/MeloNX/MeloNX/Core/Swift/Display/DisplayVisible.swift b/src/MeloNX/MeloNX/Core/Swift/Display/DisplayVisible.swift index ef423ef42..6f1f9242f 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Display/DisplayVisible.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Display/DisplayVisible.swift @@ -19,13 +19,26 @@ extension UIWindow { } self.wdb_makeKeyAndVisible() theWindow = self - if #available(iOS 15.0, *) { - reconnectVirtualController() - } - if let window = theWindow { - waitforcontroller() + if UserDefaults.standard.bool(forKey: "isVirtualController") { + if let window = theWindow { + + class LandscapeViewController: UIViewController { + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .landscape + } + + override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { + return .landscapeLeft + } + } + + let landscapeVC = LandscapeViewController() + landscapeVC.modalPresentationStyle = .fullScreen + theWindow?.rootViewController?.present(landscapeVC, animated: false, completion: nil) + waitforcontroller() + } } } } @@ -38,3 +51,4 @@ func patchMakeKeyAndVisible() { method_exchangeImplementations(m1, m2) } } + diff --git a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift index 83fa39b3b..3b2574579 100644 --- a/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift +++ b/src/MeloNX/MeloNX/Core/Swift/Ryujinx.swift @@ -7,7 +7,6 @@ import Foundation import SwiftUI -import SDL2 import GameController struct Controller: Identifiable, Hashable { @@ -32,13 +31,15 @@ struct iOSNav<Content: View>: View { class Ryujinx { private var isRunning = false + let virtualController = VirtualController() + @Published var controllerMap: [Controller] = [] static let shared = Ryujinx() private init() {} - public struct Configuration : Codable { + public struct Configuration : Codable, Equatable { var gamepath: String var inputids: [String] var resscale: Float @@ -49,7 +50,6 @@ class Ryujinx { var listinputids: Bool var fullscreen: Bool var memoryManagerMode: String - var disableVSync: Bool var disableShaderCache: Bool var disableDockedMode: Bool var enableTextureRecompression: Bool @@ -63,7 +63,6 @@ class Ryujinx { listinputids: Bool = false, fullscreen: Bool = true, memoryManagerMode: String = "HostMapped", - disableVSync: Bool = false, disableShaderCache: Bool = false, disableDockedMode: Bool = false, nintendoinput: Bool = true, @@ -78,7 +77,6 @@ class Ryujinx { self.tracelogs = tracelogs self.listinputids = listinputids self.fullscreen = fullscreen - self.disableVSync = disableVSync self.disableShaderCache = disableShaderCache self.disableDockedMode = disableDockedMode self.enableTextureRecompression = enableTextureRecompression @@ -99,7 +97,7 @@ class Ryujinx { isRunning = true // Start The Emulation on the main thread - DispatchQueue.main.async { + RunLoop.current.perform { do { let args = self.buildCommandLineArgs(from: config) @@ -145,29 +143,25 @@ class Ryujinx { args.append("--graphics-backend") args.append("Vulkan") - // Fixes the Stubs.DispatchLoop Crash - args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode]) - if config.fullscreen { - args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)]) - args.append(contentsOf: ["--exclusive-fullscreen-width", "1280"]) - args.append(contentsOf: ["--exclusive-fullscreen-height", "720"]) + args.append(contentsOf: ["--memory-manager-mode", "SoftwarePageTable"]) + + args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)]) + args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"]) + args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"]) + + + if config.nintendoinput { + // args.append("--correct-controller") } - if config.resscale != 1 { + + //args.append("--disable-vsync") + + + if config.resscale != 1.0 { args.append(contentsOf: ["--resolution-scale", String(config.resscale)]) } - if config.nintendoinput { - // args.append("--correct-ons-controller") - } - if config.enableInternet { - args.append("--enable-internet-connection") - } - - // Adding default args directly into additionalArgs - if config.disableVSync { - // args.append("--disable-vsync") - } if config.disableShaderCache { args.append("--disable-shader-cache") } @@ -204,9 +198,9 @@ class Ryujinx { } func getConnectedControllers() -> [Controller] { - var nill: String? + - guard let jsonPtr = nill else {//get_game_controllers() else { + guard let jsonPtr = get_game_controllers() else { return [] } diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib index 3253ff11f..7e3092cd5 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 3253ff11f..7e3092cd5 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/MeloNX/MeloNX/Info.plist b/src/MeloNX/MeloNX/Info.plist index ff579a6ca..8cc8fe67f 100644 --- a/src/MeloNX/MeloNX/Info.plist +++ b/src/MeloNX/MeloNX/Info.plist @@ -2,6 +2,8 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>MeloID</key> + <string></string> <key>UIFileSharingEnabled</key> <true/> </dict> diff --git a/src/MeloNX/MeloNX/MeloNXApp.swift b/src/MeloNX/MeloNX/MeloNXApp.swift index e14f820e7..d60a27413 100644 --- a/src/MeloNX/MeloNX/MeloNXApp.swift +++ b/src/MeloNX/MeloNX/MeloNXApp.swift @@ -6,12 +6,118 @@ // import SwiftUI +import UIKit @main struct MeloNXApp: App { + + @AppStorage("showeddrmcheck") var showed = true + + init() { + DispatchQueue.main.async { [self] in + // drmcheck() + if showed { + drmcheck() { bool in + if bool { + print("Yippee") + } else { + // exit(0) + } + } + } else { + showAlert() + } + } + } + var body: some Scene { WindowGroup { - ContentView() + if showed { + ContentView() + } else { + HStack { + Text("Loading...") + ProgressView() + } + } + } + } + + func showAlert() { + // 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) + } else { + exit(0) } } } + + +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) + } + + // 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) + } + +} diff --git a/src/MeloNX/MeloNX/Models/Game.swift b/src/MeloNX/MeloNX/Models/Game.swift new file mode 100644 index 000000000..cc11c5aa5 --- /dev/null +++ b/src/MeloNX/MeloNX/Models/Game.swift @@ -0,0 +1,45 @@ +// +// GameInfo.swift +// MeloNX +// +// Created by Stossy11 on 9/12/2024. +// + +import SwiftUI +import UniformTypeIdentifiers + +public struct Game: Identifiable, Equatable { + public var id = UUID() + + var containerFolder: URL + var fileType: UTType + + var fileURL: URL + + var titleName: String + var titleId: String + var developer: String + var version: String + var icon: UIImage? + + 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/Views/ContentView.swift b/src/MeloNX/MeloNX/Views/ContentView.swift index ff163bbf1..d6280c9ea 100644 --- a/src/MeloNX/MeloNX/Views/ContentView.swift +++ b/src/MeloNX/MeloNX/Views/ContentView.swift @@ -6,13 +6,16 @@ // import SwiftUI -import SDL2 +// import SDL2 import GameController +import Darwin +import UIKit +import MetalKit +// import SDL struct MoltenVKSettings: Codable, Hashable { let string: String - var bool: Bool? - var value: String? + var value: String } struct ContentView: View { @@ -25,38 +28,36 @@ struct ContentView: View { @State private var config: Ryujinx.Configuration @State private var settings: [MoltenVKSettings] @State private var isVirtualControllerActive: Bool = false + @AppStorage("isVirtualController") var isVCA: Bool = true @State var onscreencontroller: Controller = Controller(id: "", name: "") + @AppStorage("JIT") var isJITEnabled: Bool = false // MARK: - Initialization init() { - let defaultConfig = Ryujinx.Configuration(gamepath: "") + let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "") _config = State(initialValue: defaultConfig) let defaultSettings: [MoltenVKSettings] = [ - MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "1024"), - MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", value: "1"), - MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1") + MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "192"), + MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"), + MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"), + MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1"), + MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1") ] + _settings = State(initialValue: defaultSettings) + print("JIT Enabled: \(isJITEnabled)") + initializeSDL() } // MARK: - Body var body: some View { - iOSNav { - if let game { - emulationView - } else { - mainMenuView - } - } - .onChange(of: isVirtualControllerActive) { newValue in - if newValue { - createVirtualController() - } else { - destroyVirtualController() - } + if let game { + emulationView + } else { + mainMenuView } } @@ -69,138 +70,93 @@ struct ContentView: View { } private var mainMenuView: some View { - HStack { - GameListView(startemu: $game) - .onAppear { - createVirtualController() - refreshControllersList() - } - - settingsListView - } - } - - private var settingsListView: some View { - List { - Section("Settings") { - NavigationLink("Config") { - SettingsView(config: $config, MoltenVKSettings: $settings) - .onAppear() { - virtualController?.disconnect() - } - } + MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller) + .onAppear() { + refreshControllersList() } - - Section("Controller") { - Button("Refresh", action: refreshControllersList) - Divider() - ForEach(controllersList, id: \.self) { controller in - controllerRow(for: controller) - } - } - } - } - - private func controllerRow(for controller: Controller) -> some View { - HStack { - Button(controller.name) { - toggleController(controller) - } - Spacer() - if currentControllers.contains(where: { $0.id == controller.id }) { - Image(systemName: "checkmark.circle.fill") - } - } - } - - // MARK: - Controller Management - private func createVirtualController() { - let configuration = GCVirtualController.Configuration() - configuration.elements = [ - /* - GCInputLeftThumbstick, - GCInputRightThumbstick, - GCInputButtonA, - GCInputButtonB, - GCInputButtonX, - GCInputButtonY, - */ - ] - - virtualController = GCVirtualController(configuration: configuration) - virtualController?.connect() - - } - - private func destroyVirtualController() { - virtualController?.disconnect() - virtualController = nil } // MARK: - Helper Methods + var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; private func initializeSDL() { - DispatchQueue.main.async { - setMoltenVKSettings() - SDL_SetMainReady() - SDL_iPhoneSetEventPump(SDL_TRUE) - SDL_Init(SDL_INIT_VIDEO) - } + setMoltenVKSettings() + SDL_SetMainReady() + SDL_iPhoneSetEventPump(SDL_TRUE) + SDL_Init(SdlInitFlags) + // initialize() } private func setupEmulation() { virtualController?.disconnect() + patchMakeKeyAndVisible() - if controllersList.first(where: { $0 == onscreencontroller}) != nil { - controllerCallback = { - DispatchQueue.main.async { - controllersList = Ryujinx.shared.getConnectedControllers() - - print(currentControllers) - start(displayid: 1) - } - } + if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) { - - showVirtualController() - } else { + isVCA = true DispatchQueue.main.async { - print(currentControllers) start(displayid: 1) } + + + } else { + isVCA = false + + DispatchQueue.main.async { + start(displayid: 1) + } + + } } private func refreshControllersList() { - Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in - controllersList = Ryujinx.shared.getConnectedControllers() - var controller = controllersList.first(where: { $0.name.hasPrefix("Apple")}) - self.onscreencontroller = (controller ?? Controller(id: "", name: "")) - if controllersList.count > 2 { - let controller = controllersList[2] - currentControllers.append(controller) - - } else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty { - currentControllers.append(controller) - } + + + if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) { + self.onscreencontroller = onscreen } - } - - private func toggleController(_ controller: Controller) { - if currentControllers.contains(where: { $0.id == controller.id }) { - currentControllers.removeAll(where: { $0.id == controller.id }) - } else { + + controllersList.removeAll(where: { $0.id == "0"}) + + if controllersList.count > 2 { + let controller = controllersList[2] + currentControllers.append(controller) + } else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty { currentControllers.append(controller) } } + + func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) { + DispatchQueue.main.async { + if let mainWindow = UIApplication.shared.windows.last { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + + if showOk { + let okAction = UIAlertAction(title: "OK", style: .default) { _ in + completion(true) + } + + alert.addAction(okAction) + } else { + completion(false) + } + + mainWindow.rootViewController?.present(alert, animated: true, completion: nil) + } + } + } + private func start(displayid: UInt32) { guard let game else { return } config.gamepath = game.path - config.inputids = currentControllers.map(\.id) + config.inputids = Array(Set(currentControllers.map(\.id))) - allocateMemory() + if config.inputids.isEmpty { + config.inputids.append("0") + } do { try Ryujinx.shared.start(with: config) @@ -208,22 +164,9 @@ struct ContentView: View { print("Error: \(error.localizedDescription)") } } - - private func allocateMemory() { - let physicalMemory = ProcessInfo.processInfo.physicalMemory - let totalMemoryInGB = Double(physicalMemory) / (1024 * 1024 * 1024) - - let pointer = UnsafeMutableRawPointer.allocate( - byteCount: Int(totalMemoryInGB), - alignment: MemoryLayout<UInt8>.alignment - ) - pointer.initializeMemory(as: UInt8.self, repeating: 0, count: Int(totalMemoryInGB)) - } + private func setMoltenVKSettings() { - if let configs = loadSettings() { - self.config = configs - } settings.forEach { setting in setenv(setting.string, setting.value, 1) @@ -245,3 +188,4 @@ func loadSettings() -> Ryujinx.Configuration? { return nil } } + diff --git a/src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift new file mode 100644 index 000000000..03159c90d --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift @@ -0,0 +1,268 @@ +// +// ControllerView.swift +// Pomelo-V2 +// +// Created by Stossy11 on 16/7/2024. +// + +import SwiftUI +import GameController +import SwiftUIJoystick +import CoreMotion + +struct ControllerView: View { + var body: some View { + GeometryReader { geometry in + if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad { + VStack { + Spacer() + VStack { + HStack { + VStack { + ShoulderButtonsViewLeft() + ZStack { + Joystick() + DPadView() + } + } + .padding() + VStack { + ShoulderButtonsViewRight() + ZStack { + Joystick(iscool: true) // hope this works + ABXYView() + } + } + .padding() + } + + HStack { + ButtonView(button: .start) // Adding the + button + .padding(.horizontal, 40) + ButtonView(button: .back) // Adding the - button + .padding(.horizontal, 40) + } + } + .padding(.bottom, geometry.size.height / 3.2) // very broken + } + } else { + // could be landscape + VStack { + Spacer() + VStack { + HStack { + + // gotta fuckin add + and - now + VStack { + ShoulderButtonsViewLeft() + ZStack { + Joystick() + DPadView() + } + } + HStack { + // Spacer() + VStack { + // Spacer() + ButtonView(button: .back) // Adding the + button + } + Spacer() + VStack { + // Spacer() + ButtonView(button: .start) // Adding the - button + } + // Spacer() + } + VStack { + ShoulderButtonsViewRight() + ZStack { + Joystick(iscool: true) // hope this work s + ABXYView() + } + } + } + + } + // .padding(.bottom, geometry.size.height / 11) // also extremally broken ( + } + } + } + .padding() + } +} + +struct ShoulderButtonsViewLeft: View { + @State var width: CGFloat = 160 + @State var height: CGFloat = 20 + var body: some View { + HStack { + ButtonView(button: .leftTrigger) + .padding(.horizontal) + ButtonView(button: .leftShoulder) + .padding(.horizontal) + } + .frame(width: width, height: height) + .onAppear() { + if UIDevice.current.systemName.contains("iPadOS") { + width *= 1.2 + height *= 1.2 + } + } + } +} + +struct ShoulderButtonsViewRight: View { + @State var width: CGFloat = 160 + @State var height: CGFloat = 20 + var body: some View { + HStack { + ButtonView(button: .rightShoulder) + .padding(.horizontal) + ButtonView(button: .rightTrigger) + .padding(.horizontal) + } + .frame(width: width, height: height) + .onAppear() { + if UIDevice.current.systemName.contains("iPadOS") { + width *= 1.2 + height *= 1.2 + } + } + } +} + +struct DPadView: View { + @State var size: CGFloat = 145 + var body: some View { + VStack { + ButtonView(button: .dPadUp) + HStack { + ButtonView(button: .dPadLeft) + Spacer(minLength: 20) + ButtonView(button: .dPadRight) + } + ButtonView(button: .dPadDown) + .padding(.horizontal) + } + .frame(width: size, height: size) + .onAppear() { + if UIDevice.current.systemName.contains("iPadOS") { + size *= 1.2 + } + } + } +} + +struct ABXYView: View { + @State var size: CGFloat = 145 + var body: some View { + VStack { + ButtonView(button: .X) + HStack { + ButtonView(button: .Y) + Spacer(minLength: 20) + ButtonView(button: .A) + } + ButtonView(button: .B) + .padding(.horizontal) + } + .frame(width: size, height: size) + .onAppear() { + if UIDevice.current.systemName.contains("iPadOS") { + size *= 1.2 + } + } + } +} + +struct ButtonView: View { + var button: VirtualControllerButton + @State var width: CGFloat = 45 + @State var height: CGFloat = 45 + @State var isPressed = false + @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false + @Environment(\.colorScheme) var colorScheme + @Environment(\.presentationMode) var presentationMode + + + + var body: some View { + Image(systemName: buttonText) + .resizable() + .frame(width: width, height: height) + .foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) + .opacity(isPressed ? 0.4 : 0.7) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { _ in + if !self.isPressed { + self.isPressed = true + Ryujinx.shared.virtualController.setButtonState(1, for: button) + Haptics.shared.play(.heavy) + } + } + .onEnded { _ in + self.isPressed = false + Ryujinx.shared.virtualController.setButtonState(0, for: button) + } + ) + .onAppear() { + if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder { + width = 65 + } + + + if button == .back || button == .start || button == .guide { + width = 35 + height = 35 + } + + if UIDevice.current.systemName.contains("iPadOS") { + width *= 1.2 + height *= 1.2 + } + } + } + + + + private var buttonText: String { + switch button { + case .A: + return "a.circle.fill" + case .B: + return "b.circle.fill" + case .X: + return "x.circle.fill" + case .Y: + return "y.circle.fill" + case .dPadUp: + return "arrowtriangle.up.circle.fill" + case .dPadDown: + return "arrowtriangle.down.circle.fill" + case .dPadLeft: + return "arrowtriangle.left.circle.fill" + case .dPadRight: + return "arrowtriangle.right.circle.fill" + case .leftTrigger: + return"zl.rectangle.roundedtop.fill" + case .rightTrigger: + return "zr.rectangle.roundedtop.fill" + case .leftShoulder: + return "l.rectangle.roundedbottom.fill" + case .rightShoulder: + return "r.rectangle.roundedbottom.fill" + case .start: + return "plus.circle.fill" // System symbol for + + case .back: + return "minus.circle.fill" // System symbol for - + case .guide: + return "house.circle.fill" + // This should be all the cases + default: + return "" + } + } +} + + diff --git a/src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift b/src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift new file mode 100644 index 000000000..5dd555815 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift @@ -0,0 +1,27 @@ +// +// Haptics.swift +// Pomelo +// +// Created by Stossy11 on 11/9/2024. +// Copyright © 2024 Stossy11. All rights reserved. +// + +import UIKit +import SwiftUI + +class Haptics { + static let shared = Haptics() + + private init() { } + + func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { + print("haptics") + UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred() + } + + func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) { + UINotificationFeedbackGenerator().notificationOccurred(feedbackType) + } +} + + diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/Contents.json b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/Contents.json b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/Contents.json new file mode 100644 index 000000000..3a763d2f9 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "JoyStickBase@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickBase@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickBase@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@1x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@1x.png new file mode 100644 index 000000000..2e3903652 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@1x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@2x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@2x.png new file mode 100644 index 000000000..49a14c122 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@2x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@3x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@3x.png new file mode 100644 index 000000000..35851e642 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultBase.imageset/JoyStickBase@3x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/Contents.json b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/Contents.json new file mode 100644 index 000000000..6c1bf409f --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "JoyStickHandle@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickHandle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickHandle@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@1x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@1x.png new file mode 100644 index 000000000..d4555a959 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@1x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@2x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@2x.png new file mode 100644 index 000000000..93c135334 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@2x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@3x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@3x.png new file mode 100644 index 000000000..25e602e3f Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/DefaultHandle.imageset/JoyStickHandle@3x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/Contents.json b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/Contents.json new file mode 100644 index 000000000..6f901e4e8 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "JoyStickBaseCustom@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickBaseCustom@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickBaseCustom@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@1x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@1x.png new file mode 100644 index 000000000..113ccadc4 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@1x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@2x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@2x.png new file mode 100644 index 000000000..bbdf7e4cd Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@2x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@3x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@3x.png new file mode 100644 index 000000000..949788e5f Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyBase.imageset/JoyStickBaseCustom@3x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/Contents.json b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/Contents.json new file mode 100644 index 000000000..4091d8b19 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "JoyStickHandleCustom@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickHandleCustom@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "JoyStickHandleCustom@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@1x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@1x.png new file mode 100644 index 000000000..9fb451158 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@1x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@2x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@2x.png new file mode 100644 index 000000000..2c0f0d930 Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@2x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@3x.png b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@3x.png new file mode 100644 index 000000000..edc88f2ee Binary files /dev/null and b/src/MeloNX/MeloNX/Views/ControllerView/JoyStickView/Resources/Assets.xcassets/FancyHandle.imageset/JoyStickHandleCustom@3x.png differ diff --git a/src/MeloNX/MeloNX/Views/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/Views/ControllerView/Joystick/JoystickView.swift new file mode 100644 index 000000000..dc1db3d8c --- /dev/null +++ b/src/MeloNX/MeloNX/Views/ControllerView/Joystick/JoystickView.swift @@ -0,0 +1,53 @@ +// +// JoystickView.swift +// Pomelo +// +// Created by Stossy11 on 30/9/2024. +// Copyright © 2024 Stossy11. All rights reserved. +// + +import SwiftUI +import SwiftUIJoystick + +public struct Joystick: View { + @State var iscool: Bool? = nil + + @ObservedObject public var joystickMonitor = JoystickMonitor() + var dragDiameter: CGFloat { + var selfs = CGFloat(160) + if UIDevice.current.systemName.contains("iPadOS") { + return selfs * 1.2 + } + return selfs + } + private let shape: JoystickShape = .circle + + public var body: some View { + VStack{ + JoystickBuilder( + monitor: self.joystickMonitor, + width: self.dragDiameter, + shape: .circle, + background: { + Text("") + .hidden() + }, + foreground: { + Circle().fill(Color.gray) + .opacity(0.7) + }, + locksInPlace: false) + .onChange(of: self.joystickMonitor.xyPoint) { newValue in + let scaledX = Float(newValue.x) + let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/ + print("Joystick Position: (\(scaledX), \(scaledY))") + + if iscool != nil { + Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y) + } else { + Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y) + } + } + } + } +} diff --git a/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift index ec8891526..35d4c42c8 100644 --- a/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/Views/GamesList/GameListView.swift @@ -5,25 +5,147 @@ // Created by Stossy11 on 3/11/2024. // -// MARK: - This will most likely not be used in prod import SwiftUI +import UniformTypeIdentifiers -struct GameListView: View { + +struct GameLibraryView: View { @Binding var startemu: URL? - @State private var games: [URL] = [] - + @State private var games: [Game] = [] + @State private var searchText = "" + @State private var isSearching = false + @AppStorage("recentGames") private var recentGamesData: Data = Data() + @State private var recentGames: [Game] = [] + @Environment(\.colorScheme) var colorScheme + + var filteredGames: [Game] { + if searchText.isEmpty { + return games + } + return games.filter { + $0.titleName.localizedCaseInsensitiveContains(searchText) || + $0.developer.localizedCaseInsensitiveContains(searchText) + } + } + var body: some View { - List(games, id: \.self) { game in - Button { - startemu = game - } label: { - Text(game.lastPathComponent) + iOSNav { + ScrollView { + LazyVStack(alignment: .leading, spacing: 20) { + if !isSearching { + Text("Games") + .font(.system(size: 34, weight: .bold)) + .padding(.horizontal) + .padding(.top, 12) + } + + if games.isEmpty { + VStack(spacing: 16) { + Image(systemName: "gamecontroller.fill") + .font(.system(size: 64)) + .foregroundColor(.secondary.opacity(0.7)) + .padding(.top, 60) + Text("No Games Found") + .font(.title2.bold()) + .foregroundColor(.primary) + Text("Add ROM, Keys and Firmware to get started") + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.top, 40) + } else { + if !isSearching && !recentGames.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Recent") + .font(.title2.bold()) + .padding(.horizontal) + + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + ForEach(recentGames) { game in + RecentGameCard(game: game, startemu: $startemu) + .onTapGesture { + addToRecentGames(game) + startemu = game.fileURL + } + } + } + .padding(.horizontal) + } + } + + VStack(alignment: .leading, spacing: 12) { + Text("All Games") + .font(.title2.bold()) + .padding(.horizontal) + + LazyVStack(spacing: 2) { + ForEach(filteredGames) { game in + GameListRow(game: game, startemu: $startemu) + .onTapGesture { + addToRecentGames(game) + } + } + } + } + } else { + LazyVStack(spacing: 2) { + ForEach(filteredGames) { game in + GameListRow(game: game, startemu: $startemu) + .onTapGesture { + addToRecentGames(game) + } + } + } + } + } + } + .onAppear { + loadGames() + loadRecentGames() + } } } - .navigationTitle("Games") - .onAppear(perform: loadGames) + .background(Color(.systemGroupedBackground)) + .searchable(text: $searchText) + .onChange(of: searchText) { _ in + isSearching = !searchText.isEmpty + } } - + + private func addToRecentGames(_ game: Game) { + recentGames.removeAll { $0.id == game.id } + + recentGames.insert(game, at: 0) + + if recentGames.count > 5 { + recentGames = Array(recentGames.prefix(5)) + } + + saveRecentGames() + } + + private func saveRecentGames() { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(recentGames) + recentGamesData = data + } catch { + print("Error saving recent games: \(error)") + } + } + + private func loadRecentGames() { + do { + let decoder = JSONDecoder() + recentGames = try decoder.decode([Game].self, from: recentGamesData) + } catch { + print("Error loading recent games: \(error)") + recentGames = [] + } + } + private func loadGames() { let fileManager = FileManager.default guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return } @@ -38,13 +160,187 @@ struct GameListView: View { print("Failed to create roms directory: \(error)") } } - + games = [] // Load games only from "roms" folder do { let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil) - games = files + + files.forEach { fileURLCandidate in + do { + let handle = try FileHandle(forReadingFrom: fileURLCandidate) + let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String + let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension) + + + var game = Game(containerFolder: romsDirectory, fileType: .item, fileURL: fileURLCandidate, titleName: fileURLCandidate.lastPathComponent, titleId: "", developer: "", version: "") + + /* + game.titleName = withUnsafePointer(to: &gameInfo.TitleName) { + $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { + String(cString: $0) + } + } + + game.developer = withUnsafePointer(to: &gameInfo.Developer) { + $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { + String(cString: $0) + } + } + */ + + + games.append(game) + } catch { + print(error) + } + } + } catch { print("Error loading games from roms folder: \(error)") } } } + +// Make sure your Game model conforms to Codable +extension Game: Codable { + enum CodingKeys: String, CodingKey { + case titleName, titleId, developer, version, fileURL + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + titleName = try container.decode(String.self, forKey: .titleName) + titleId = try container.decode(String.self, forKey: .titleId) + developer = try container.decode(String.self, forKey: .developer) + version = try container.decode(String.self, forKey: .version) + fileURL = try container.decode(URL.self, forKey: .fileURL) + + // Initialize other properties + self.containerFolder = fileURL.deletingLastPathComponent() + self.fileType = .item + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(titleName, forKey: .titleName) + try container.encode(titleId, forKey: .titleId) + try container.encode(developer, forKey: .developer) + try container.encode(version, forKey: .version) + try container.encode(fileURL, forKey: .fileURL) + } +} + +struct RecentGameCard: View { + let game: Game + @Binding var startemu: URL? + @Environment(\.colorScheme) var colorScheme + + var body: some View { + Button(action: { + startemu = game.fileURL + }) { + VStack(alignment: .leading, spacing: 8) { + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 140, height: 140) + .cornerRadius(12) + } else { + ZStack { + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? + Color(.systemGray5) : Color(.systemGray6)) + .frame(width: 140, height: 140) + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 40)) + .foregroundColor(.gray) + } + } + + VStack(alignment: .leading, spacing: 2) { + Text(game.titleName) + .font(.subheadline.bold()) + .lineLimit(1) + + Text(game.developer) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + .padding(.horizontal, 4) + } + } + .buttonStyle(.plain) + } +} + +struct GameListRow: View { + let game: Game + @Binding var startemu: URL? + @Environment(\.colorScheme) var colorScheme + + var body: some View { + Button(action: { + startemu = game.fileURL + }) { + HStack(spacing: 16) { + // Game Icon + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 45, height: 45) + .cornerRadius(8) + } else { + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(colorScheme == .dark ? + Color(.systemGray5) : Color(.systemGray6)) + .frame(width: 45, height: 45) + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 20)) + .foregroundColor(.gray) + } + } + + // Game Info + VStack(alignment: .leading, spacing: 2) { + Text(game.titleName) + .font(.body) + .foregroundColor(.primary) + + Text(game.developer) + .font(.subheadline) + .foregroundColor(.secondary) + } + + Spacer() + + Image(systemName: "play.circle.fill") + .font(.title2) + .foregroundColor(.accentColor) + .opacity(0.8) + } + .padding(.horizontal) + .padding(.vertical, 8) + .background(Color(.systemBackground)) + .contextMenu { + Button { + startemu = game.fileURL + } label: { + Label("Play Now", systemImage: "play.fill") + } + + Button { + // Add info action + } label: { + Label("Game Info", systemImage: "info.circle") + } + } + } + .buttonStyle(.plain) + } +} diff --git a/src/MeloNX/MeloNX/Views/SDLView/SDLView.swift b/src/MeloNX/MeloNX/Views/SDLView/SDLView.swift deleted file mode 100644 index 1f89ffaf8..000000000 --- a/src/MeloNX/MeloNX/Views/SDLView/SDLView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// VulkanSDLView.swift -// MeloNX -// -// Created by Stossy11 on 3/11/2024. -// - -import UIKit -import MetalKit -import SDL2 - -class SDLView: UIView { - var sdlwin: OpaquePointer? - - override init(frame: CGRect) { - super.init(frame: frame) - DispatchQueue.main.async { [self] in - makeSDLWindow() - } - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - DispatchQueue.main.async { [self] in - makeSDLWindow() - } - } - - func getWindowFlags() -> UInt32 { - return SDL_WINDOW_VULKAN.rawValue - } - - private func makeSDLWindow() { - let width: Int32 = 1280 // Replace with the desired width - let height: Int32 = 720 // Replace with the desired height - - let defaultFlags: UInt32 = SDL_WINDOW_SHOWN.rawValue - let fullscreenFlag: UInt32 = SDL_WINDOW_FULLSCREEN.rawValue // Or SDL_WINDOW_FULLSCREEN_DESKTOP if needed - - // Create the SDL window - sdlwin = SDL_CreateWindow( - "Ryujinx", - 0, - 0, - width, - height, - defaultFlags | getWindowFlags() // | fullscreenFlag | getWindowFlags() - ) - - // Check if we successfully retrieved the SDL window - guard sdlwin != nil else { - print("Error creating SDL window: \(String(cString: SDL_GetError()))") - return - } - - print("SDL window created successfully.") - - // Position SDL window over this UIView - self.syncSDLWindowPosition() - } - - private func syncSDLWindowPosition() { - guard let sdlwin = sdlwin else { return } - - - // Get the frame of the UIView in screen coordinates - let viewFrameInWindow = self.convert(self.bounds, to: nil) - - // Set the SDL window position and size to match the UIView frame - SDL_SetWindowPosition(sdlwin, Int32(viewFrameInWindow.origin.x), Int32(viewFrameInWindow.origin.y)) - SDL_SetWindowSize(sdlwin, Int32(viewFrameInWindow.width), Int32(viewFrameInWindow.height)) - - // Bring SDL window to the front - SDL_RaiseWindow(sdlwin) - - print("SDL window positioned over SDLView.") - } - - override func layoutSubviews() { - super.layoutSubviews() - // Adjust SDL window whenever layout changes - syncSDLWindowPosition() - } -} diff --git a/src/MeloNX/MeloNX/Views/SDLView/SDLViewRepresentable.swift b/src/MeloNX/MeloNX/Views/SDLView/SDLViewRepresentable.swift deleted file mode 100644 index be04a2581..000000000 --- a/src/MeloNX/MeloNX/Views/SDLView/SDLViewRepresentable.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// VulkanSDLViewRepresentable.swift -// MeloNX -// -// Created by Stossy11 on 3/11/2024. -// - -import UIKit -import SwiftUI -import SDL2 -import GameController - -struct SDLViewRepresentable: UIViewRepresentable { - let configure: (UInt32) -> Void - func makeUIView(context: Context) -> SDLView { - // Configure (start ryu) before initialsing SDLView so SDLView can get the SDL_Window from Ryu - let view = SDLView(frame: .zero) - configure(SDL_GetWindowID(view.sdlwin)) - return view - - } - - func updateUIView(_ uiView: SDLView, context: Context) { - - } - -} diff --git a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift index ddd89965f..3defbcf63 100644 --- a/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/Views/SettingsView/SettingsView.swift @@ -11,6 +11,13 @@ struct SettingsView: View { @Binding var config: Ryujinx.Configuration @Binding var MoltenVKSettings: [MoltenVKSettings] + @Binding var controllersList: [Controller] + @Binding var currentControllers: [Controller] + + @Binding var onscreencontroller: Controller + + @AppStorage("ignoreJIT") var ignoreJIT: Bool = false + var memoryManagerModes = [ ("HostMapped", "Host (fast)"), ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"), @@ -18,166 +25,294 @@ struct SettingsView: View { ] @AppStorage("RyuDemoControls") var ryuDemo: Bool = false - @AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false + @State private var showResolutionInfo = false + @State private var searchText = "" + + var filteredMemoryModes: [(String, String)] { + guard !searchText.isEmpty else { return memoryManagerModes } + return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) } + } + var body: some View { - ScrollView { - VStack { - Section(header: Title("Graphics and Performance")) { - Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) - Toggle("Disable V-Sync", isOn: $config.disableVSync) - Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) - Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) - Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) - Resolution(value: $config.resscale) - Toggle("Enable Metal HUD", isOn: $metalHUDEnabled) - .onChange(of: metalHUDEnabled) { newValue in - if newValue { - MTLHud.shared.enable() - } else { - MTLHud.shared.disable() + iOSNav { + List { + // Graphics & Performance + Section { + Toggle(isOn: $config.fullscreen) { + labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical") + } + .tint(.blue) + + Toggle(isOn: $config.disableShaderCache) { + labelWithIcon("Disable Shader Cache", iconName: "memorychip") + } + .tint(.blue) + + Toggle(isOn: $config.enableTextureRecompression) { + labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical") + } + .tint(.blue) + + Toggle(isOn: $config.disableDockedMode) { + labelWithIcon("Disable Docked Mode", iconName: "dock.rectangle") + } + .tint(.blue) + + VStack(alignment: .leading, spacing: 10) { + HStack { + labelWithIcon("Resolution Scale", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showResolutionInfo.toggle() + } label: { + Image(systemName: "info.circle") + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .help("Learn more about Resolution Scale") + .alert(isPresented: $showResolutionInfo) { + Alert( + title: Text("Resolution Scale"), + message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."), + dismissButton: .default(Text("OK")) + ) } } + + Slider(value: $config.resscale, in: 0.1...3.0, step: 0.1) { + Text("Resolution Scale") + } minimumValueLabel: { + Text("0.1x") + .font(.footnote) + .foregroundColor(.secondary) + } maximumValueLabel: { + Text("3.0x") + .font(.footnote) + .foregroundColor(.secondary) + } + Text("\(config.resscale, specifier: "%.2f")x") + .font(.subheadline) + .foregroundColor(.secondary) + } + .padding(.vertical, 8) + + Toggle(isOn: $metalHUDEnabled) { + labelWithIcon("Metal HUD", iconName: "speedometer") + } + .tint(.blue) + .onChange(of: metalHUDEnabled) { newValue in + // Preserves original functionality + if newValue { + MTLHud.shared.enable() + } else { + MTLHud.shared.disable() + } + } + } header: { + Text("Graphics & Performance") + .font(.title3.weight(.semibold)) + .textCase(nil) + .headerProminence(.increased) + } footer: { + Text("Fine-tune graphics and performance to suit your device and preferences.") } - Section(header: Title("Input Settings")) { - Toggle("List Input IDs", isOn: $config.listinputids) - Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput) - Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo) - // Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory) + // Input Selector + Section { + ForEach(controllersList) { controller in + var customBinding: Binding<Bool> { + Binding( + get: { currentControllers.contains(controller) }, + set: { bool in + if !bool { + currentControllers.removeAll(where: { $0.id == controller.id }) + } else { + currentControllers.append(controller) + } + // toggleController(controller) + } + ) + } + + Toggle(isOn: customBinding) { + labelWithIcon(controller.name, iconName: "") + } + .tint(.blue) + } + } header: { + Text("Input Selector") + .font(.title3.weight(.semibold)) + .textCase(nil) + .headerProminence(.increased) + } footer: { + Text("Select input devices and on-screen controls to play with.") } - - Section(header: Title("Logging Settings")) { - Toggle("Enable Debug Logs", isOn: $config.debuglogs) - Toggle("Enable Trace Logs", isOn: $config.tracelogs) + + // Input Settings + Section { + + Toggle(isOn: $config.listinputids) { + labelWithIcon("List Input IDs", iconName: "list.bullet") + } + .tint(.blue) + + Toggle(isOn: $ryuDemo) { + labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") + } + .tint(.blue) + .disabled(true) + } header: { + Text("Input Settings") + .font(.title3.weight(.semibold)) + .textCase(nil) + .headerProminence(.increased) + } footer: { + Text("Configure input devices and on-screen controls for easier navigation and play.") } - Section(header: Title("CPU Mode")) { - HStack { - Spacer() - Picker("Memory Manager Mode", selection: $config.memoryManagerMode) { - ForEach(memoryManagerModes, id: \.0) { key, displayName in + + // Logging + Section { + Toggle(isOn: $config.debuglogs) { + labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble") + } + .tint(.blue) + + Toggle(isOn: $config.tracelogs) { + labelWithIcon("Trace Logs", iconName: "waveform.path") + } + .tint(.blue) + } header: { + Text("Logging") + .font(.title3.weight(.semibold)) + .textCase(nil) + .headerProminence(.increased) + } footer: { + Text("Enable logs for troubleshooting or keep them off for a cleaner experience.") + } + + // CPU Mode + Section { + if filteredMemoryModes.isEmpty { + Text("No matches for \"\(searchText)\"") + .foregroundColor(.secondary) + } else { + Picker(selection: $config.memoryManagerMode) { + ForEach(filteredMemoryModes, id: \.0) { key, displayName in Text(displayName).tag(key) } + } label: { + labelWithIcon("Memory Manager Mode", iconName: "gearshape") } - .pickerStyle(MenuPickerStyle()) // Dropdown style } + } header: { + Text("CPU Mode") + .font(.title3.weight(.semibold)) + .textCase(nil) + .headerProminence(.increased) + } footer: { + Text("Select how memory is managed. 'Host (fast)' is best for most users.") } - - - - Section(header: Title("Additional Settings")) { - //TextField("Game Path", text: $config.gamepath) - - Text("PageSize \(String(Int(getpagesize())))") - - TextField("Additional Arguments", text: Binding( - get: { - config.additionalArgs.joined(separator: ", ") - }, - set: { newValue in - config.additionalArgs = newValue.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } + + // Advanced + Section { + DisclosureGroup { + HStack { + labelWithIcon("Page Size", iconName: "textformat.size") + Spacer() + Text("\(String(Int(getpagesize())))") + .foregroundColor(.secondary) } - )) + + TextField("Additional Arguments", text: Binding( + get: { + config.additionalArgs.joined(separator: ", ") + }, + set: { newValue in + config.additionalArgs = newValue + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + } + )) + .textInputAutocapitalization(.none) + .disableAutocorrection(true) + } label: { + Text("Advanced Options") + } + } header: { + Text("Advanced") + .font(.title3.weight(.semibold)) + .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)") } } - .padding() - } - .onAppear { - if let configs = loadSettings() { - self.config = configs - print(configs) + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) + .listStyle(.insetGrouped) + .onAppear { + if let configs = loadSettings() { + self.config = configs + } + } + .onChange(of: config) { _ in + saveSettings() } } - .navigationTitle("Settings") - .navigationBarItems(trailing: Button("Save") { - saveSettings() - }) + .navigationViewStyle(.stack) + } + + private func toggleController(_ controller: Controller) { + if currentControllers.contains(where: { $0.id == controller.id }) { + currentControllers.removeAll(where: { $0.id == controller.id }) + } else { + currentControllers.append(controller) + } } func saveSettings() { do { let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted // Optional: Makes the JSON easier to read + encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(config) let jsonString = String(data: data, encoding: .utf8) - - // Save to UserDefaults UserDefaults.standard.set(jsonString, forKey: "config") - - print("Settings saved successfully!") } catch { print("Failed to save settings: \(error)") } } -} - - -struct Resolution: View { - @Binding var value: Float - - var body: some View { - HStack { - Text("Resolution Scale (Custom):") - Spacer() - - Button(action: { - if value > 0.1 { // Prevent values going below 0.1 - value -= 0.10 - value = round(value * 1000) / 1000 // Round to two decimal places - } - print(value) - }) { - Text("-") - .frame(width: 30, height: 30) - .background(Color.gray.opacity(0.2)) - .cornerRadius(5) - } - - TextField("", value: $value, formatter: NumberFormatter.floatFormatter) - .multilineTextAlignment(.center) - .frame(width: 60) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .keyboardType(.decimalPad) - - Button(action: { - value += 0.10 - value = round(value * 1000) / 1000 // Round to two decimal places - print(value) - }) { - Text("+") - .frame(width: 30, height: 30) - .background(Color.gray.opacity(0.2)) - .cornerRadius(5) - } + + // Original loadSettings function assumed to exist + func loadSettings() -> Ryujinx.Configuration? { + guard let jsonString = UserDefaults.standard.string(forKey: "config"), + let data = jsonString.data(using: .utf8) else { + return nil + } + do { + let decoder = JSONDecoder() + let configs = try decoder.decode(Ryujinx.Configuration.self, from: data) + return configs + } catch { + print("Failed to load settings: \(error)") + return nil } } -} - -extension NumberFormatter { - static var floatFormatter: NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 2 - formatter.minimumFractionDigits = 2 - formatter.allowsFloats = true - return formatter - } -} - - -struct Title: View { - let string: String - init(_ string: String) { - self.string = string - } - - var body: some View { - VStack { - Text(string) - .font(.title2) - Divider() + @ViewBuilder + private func labelWithIcon(_ text: String, iconName: String) -> some View { + HStack(spacing: 8) { + if !iconName.isEmpty { + Image(systemName: iconName) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.blue) + } + Text(text) } + .font(.body) } } diff --git a/src/MeloNX/MeloNX/Views/TabView/TabView.swift b/src/MeloNX/MeloNX/Views/TabView/TabView.swift new file mode 100644 index 000000000..21b52e907 --- /dev/null +++ b/src/MeloNX/MeloNX/Views/TabView/TabView.swift @@ -0,0 +1,34 @@ +// +// TabView.swift +// MeloNX +// +// Created by Stossy11 on 10/12/2024. +// + +import SwiftUI +import UniformTypeIdentifiers + + +struct MainTabView: View { + @Binding var startemu: URL? + @Binding var config: Ryujinx.Configuration + @Binding var MVKconfig: [MoltenVKSettings] + @Binding var controllersList: [Controller] + @Binding var currentControllers: [Controller] + + @Binding var onscreencontroller: Controller + + var body: some View { + TabView { + GameLibraryView(startemu: $startemu) + .tabItem { + Label("Games", systemImage: "gamecontroller.fill") + } + + SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller) + .tabItem { + Label("Settings", systemImage: "gear") + } + } + } +} diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index d0d811923..644b8cafa 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -65,7 +65,12 @@ namespace Ryujinx.Common.Configuration appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); } - string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir); + string userProfilePath; + if (OperatingSystem.IsIOS()) { + userProfilePath = appDataPath; + } else { + userProfilePath = Path.Combine(appDataPath, DefaultBaseDir); + } string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir); // On macOS, check for a portable directory next to the app bundle as well. diff --git a/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs index a04c404d8..f2d1d857d 100644 --- a/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs +++ b/src/Ryujinx.Common/SystemInterop/StdErrAdapter.cs @@ -27,6 +27,7 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] private void RegisterPosix() { const int StdErrFileno = 2; @@ -44,6 +45,7 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] private async Task EventWorkerAsync(CancellationToken cancellationToken) { using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true); @@ -92,6 +94,7 @@ namespace Ryujinx.Common.SystemInterop [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] private static Stream CreateFileDescriptorStream(int fd) { return new FileStream( @@ -100,5 +103,6 @@ namespace Ryujinx.Common.SystemInterop ); } + } } diff --git a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs index d7990ad58..7d4af43a1 100644 --- a/src/Ryujinx.Common/Utilities/EmbeddedResources.cs +++ b/src/Ryujinx.Common/Utilities/EmbeddedResources.cs @@ -134,20 +134,12 @@ namespace Ryujinx.Common private static (Assembly, string) ResolveManifestPath(string filename) { - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; var segments = filename.Split('/', 2, StringSplitOptions.RemoveEmptyEntries); if (segments.Length >= 2) { - foreach (var assembly in System.Runtime.Loader.AssemblyLoadContext.Default.Assemblies) - { - if (assembly.GetName().Name == segments[0]) - { - return (assembly, segments[1]); - } - } + var assembly = Assembly.GetExecutingAssembly(); + return (assembly, segments[1]); } return (_resourceAssembly, filename); diff --git a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs index 2985f1c21..59a5b4d74 100644 --- a/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs +++ b/src/Ryujinx.Cpu/Signal/NativeSignalHandler.cs @@ -88,7 +88,7 @@ namespace Ryujinx.Cpu.Signal ref SignalHandlerConfig config = ref GetConfigRef(); - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) { _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize)); diff --git a/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs b/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs index d40e7cdc9..c380f43fd 100644 --- a/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs +++ b/src/Ryujinx.Cpu/Signal/UnixSignalHandlerRegistration.cs @@ -62,7 +62,7 @@ namespace Ryujinx.Cpu.Signal throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}"); } - if (OperatingSystem.IsMacOS()) + if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) { result = sigaction(SIGBUS, ref sig, out _); @@ -77,7 +77,7 @@ namespace Ryujinx.Cpu.Signal public static bool RestoreExceptionHandler(SigAction oldAction) { - return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0); + return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0); } } } diff --git a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs index 086c4e1df..931422279 100644 --- a/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/MoltenVK/MVKInitialization.cs @@ -7,6 +7,7 @@ using System.Runtime.Versioning; namespace Ryujinx.Graphics.Vulkan.MoltenVK { [SupportedOSPlatform("macos")] + [SupportedOSPlatform("ios")] public static partial class MVKInitialization { private const string VulkanLib = "libvulkan.dylib"; diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index cc2bc36c2..a127c27ef 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -14,6 +14,14 @@ using System.Runtime.InteropServices; using Format = Ryujinx.Graphics.GAL.Format; using PrimitiveTopology = Ryujinx.Graphics.GAL.PrimitiveTopology; using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; +using System.Globalization; +using System.Threading; +using System; +using System.Globalization; +using System.Threading; +using System.Resources; +using System.Reflection; + namespace Ryujinx.Graphics.Vulkan { @@ -498,6 +506,33 @@ namespace Ryujinx.Graphics.Vulkan Queue = queue; QueueLock = new object(); + try + { + // Set invariant culture + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var assemblyName = new AssemblyName(args.Name); + assemblyName.CultureInfo = CultureInfo.InvariantCulture; + try + { + return Assembly.Load(assemblyName); + } + catch + { + return null; + } + }; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to set culture: {ex.Message}"); + } + LoadFeatures(maxQueueCount, queueFamilyIndex); QueueFamilyIndex = queueFamilyIndex; diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs index 87d88fc65..6bed14279 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -98,7 +98,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error SystemLanguage.CanadianFrench => "fr-CA", SystemLanguage.LatinAmericanSpanish => "es-419", SystemLanguage.SimplifiedChinese => "zh-Hans", - SystemLanguage.TraditionalChinese => "zh-Hant", SystemLanguage.BrazilianPortuguese => "pt-BR", _ => "en-US", #pragma warning restore IDE0055 diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs index c650fe036..8e68975f3 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs @@ -21,7 +21,6 @@ namespace Ryujinx.HLE.HOS.SystemState "fr-CA", "es-419", "zh-Hans", - "zh-Hant", "pt-BR", }; diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index a58f4299f..d6bf938d3 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -102,10 +102,6 @@ namespace Ryujinx.Headless.SDL2 Version = "1"; // Make process DPI aware for proper window sizing on high-res screens. ForceDpiAware.Windows(); - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS; diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index c12ee5eb3..b6f1a67de 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -48,7 +48,7 @@ namespace Ryujinx.Headless.SDL2 public NpadManager NpadManager { get; } public TouchScreenManager TouchScreenManager { get; } - public Switch Device { get; private set; } + public Switch Device; public IRenderer Renderer { get; private set; } public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; diff --git a/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs b/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs index dc9712692..759453b69 100644 --- a/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs +++ b/src/Ryujinx.Horizon/Sdk/Settings/LanguageCode.cs @@ -26,7 +26,6 @@ namespace Ryujinx.Horizon.Sdk.Settings "fr-CA", "es-419", "zh-Hans", - "zh-Hant", "pt-BR" }; diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs index c203834f5..08e8f940f 100644 --- a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs +++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs @@ -73,7 +73,7 @@ namespace Ryujinx.UI.Common.Helper /// <returns>A formatted string that can be displayed in the UI.</returns> public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) { - culture ??= CultureInfo.CurrentCulture; + culture ??= CultureInfo.InvariantCulture; return utcDateTime?.ToLocalTime().ToString(culture); } @@ -159,7 +159,7 @@ namespace Ryujinx.UI.Common.Helper /// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns> public static DateTime ParseDateTime(string dateTimeString) { - if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime)) + if (!DateTime.TryParse(dateTimeString, CultureInfo.InvariantCulture, out DateTime parsedDateTime)) { // Games that were never played are supposed to appear before the oldest played games in the list, // so returning DateTime.UnixEpoch here makes sense. diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 824fdd717..dce24e31d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1154,7 +1154,7 @@ namespace Ryujinx.Ava.UI.ViewModels return true; } - CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo; + CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo; return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0; }