Latest Changes and almost works

This commit is contained in:
Stossy11 2024-12-21 19:52:28 +11:00
parent ba3f6abb4c
commit 5e0d752851
59 changed files with 1814 additions and 539 deletions

View File

@ -19,8 +19,7 @@ namespace ARMeilleure.CodeGen.Arm64
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP); LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2); LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2);
} }
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
if (OperatingSystem.IsMacOS())
{ {
for (int i = 0; i < _sysctlNames.Length; i++) 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); private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, nint newValue, ulong newValueSize);
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
private static bool CheckSysctlName(string name) private static bool CheckSysctlName(string name)
{ {
ulong size = sizeof(int); ulong size = sizeof(int);

View File

@ -87,13 +87,13 @@ namespace ARMeilleure.Signal
private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr) 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))); return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset)));
} }
private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr) private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr)
{ {
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{ {
const ulong McontextOffset = 48; // uc_mcontext const ulong McontextOffset = 48; // uc_mcontext
Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset))); Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset)));

View File

@ -30,21 +30,26 @@ namespace ARMeilleure.Translation.Cache
_blocks.Add(new MemoryBlock(0, capacity)); _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++) for (int i = 0; i < _blocks.Count; i++)
{ {
MemoryBlock block = _blocks[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); size = alignedSize;
return block.Offset; _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); _blocks.RemoveAt(i);
return block.Offset; return block.Offset + misAlignment;
} }
} }

View File

@ -47,7 +47,7 @@ namespace ARMeilleure.Translation.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, CacheSize); _jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{ {
@ -80,6 +80,7 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(funcOffset, code.Length);
@ -119,6 +120,13 @@ namespace ARMeilleure.Translation.Cache
public static void Unmap(nint pointer) public static void Unmap(nint pointer)
{ {
if (OperatingSystem.IsIOS())
{
return;
}
lock (_lock) lock (_lock)
{ {
Debug.Assert(_initialized); Debug.Assert(_initialized);
@ -157,7 +165,18 @@ namespace ARMeilleure.Translation.Cache
{ {
codeSize = AlignCodeSize(codeSize); 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) if (allocOffset < 0)
{ {
@ -171,6 +190,13 @@ namespace ARMeilleure.Translation.Cache
private static int AlignCodeSize(int codeSize) private static int AlignCodeSize(int codeSize)
{ {
int alignment = CodeAlignment;
if (OperatingSystem.IsIOS())
{
alignment = 0x4000;
}
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1); return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
} }

View File

@ -1022,6 +1022,7 @@ namespace ARMeilleure.Translation.PTC
osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1; osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1;
osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2; osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2;
osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3; osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3;
osPlatform |= (OperatingSystem.IsIOS() ? 1u : 0u) << 4;
#pragma warning restore IDE0055 #pragma warning restore IDE0055
return osPlatform; return osPlatform;

View File

@ -62,7 +62,6 @@
4E6715F12CFEEB6E00425F0C /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = { 4E6715F12CFEEB6E00425F0C /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = {
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
attributesByRelativePath = { attributesByRelativePath = {
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (CodeSignOnCopy, );
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (CodeSignOnCopy, );
"Dependencies/Dynamic Libraries/libavcodec.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libavcodec.dylib" = (CodeSignOnCopy, );
"Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, ); "Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, );
@ -82,7 +81,6 @@
"Dependencies/Dynamic Libraries/libavcodec.dylib", "Dependencies/Dynamic Libraries/libavcodec.dylib",
"Dependencies/Dynamic Libraries/libavutil.dylib", "Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib", "Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
Dependencies/XCFrameworks/libavcodec.xcframework, Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;

View File

@ -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);

View File

@ -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
}
}

View File

@ -8,13 +8,29 @@
#ifndef RyujinxHeader #ifndef RyujinxHeader
#define RyujinxHeader #define RyujinxHeader
#import "SDL2/SDL.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #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 // Declare the main_ryujinx_sdl function, matching the signature
int main_ryujinx_sdl(int argc, char **argv); int main_ryujinx_sdl(int argc, char **argv);
// void initialize();
const char* get_game_controllers(); const char* get_game_controllers();
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
class MTLHud { class MTLHud {
var canMetalHud: Bool { var canMetalHud: Bool {

View File

@ -2,98 +2,188 @@
// VirtualController.swift // VirtualController.swift
// MeloNX // MeloNX
// //
// Created by Stossy11 on 28/11/2024. // Created by Stossy11 on 8/12/2024.
// //
import Foundation import Foundation
import GameController import CoreHaptics
import UIKit import UIKit
public var controllerCallback: (() -> Void)? class VirtualController {
private var instanceID: SDL_JoystickID = -1
var VirtualController: GCVirtualController! private var controller: OpaquePointer?
func showVirtualController() {
let config = GCVirtualController.Configuration() public let controllername = "MeloNX Touch Controller"
if UserDefaults.standard.bool(forKey: "RyuDemoControls") {
config.elements = [ init() {
GCInputLeftThumbstick, setupVirtualController()
GCInputButtonA,
GCInputButtonB,
GCInputButtonX,
GCInputButtonY,
// GCInputRightThumbstick,
GCInputRightTrigger,
GCInputLeftTrigger,
GCInputLeftShoulder,
GCInputRightShoulder
]
} else {
config.elements = [
GCInputLeftThumbstick,
GCInputButtonA,
GCInputButtonB,
GCInputButtonX,
GCInputButtonY,
GCInputRightThumbstick,
GCInputRightTrigger,
GCInputLeftTrigger,
GCInputLeftShoulder,
GCInputRightShoulder
]
} }
VirtualController = GCVirtualController(configuration: config)
VirtualController.connect { err in private func setupVirtualController() {
print("controller connect: \(String(describing: err))") // Initialize SDL if not already initialized
patchMakeKeyAndVisible() if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
if let controllerCallback { SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
controllerCallback()
} }
}
}
func waitforcontroller() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
if let window = UIApplication.shared.windows.first { // Create virtual controller
// Function to recursively search for GCControllerView var joystickDesc = SDL_VirtualJoystickDesc(
func findGCControllerView(in view: UIView) -> UIView? { version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
// Check if current view is GCControllerView type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
if String(describing: type(of: view)) == "GCControllerView" { naxes: 6,
return view 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 { instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if let found = findGCControllerView(in: subview) { if instanceID < 0 {
return found print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
} return
}
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)
}
}
} }
// 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, *) enum VirtualControllerButton: Int {
func reconnectVirtualController() { case B
VirtualController.disconnect() case A
DispatchQueue.main.async { case Y
VirtualController.connect { err in case X
print("reconnected: err \(String(describing: err))") 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
}

View File

@ -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
}
}

View File

@ -19,13 +19,26 @@ extension UIWindow {
} }
self.wdb_makeKeyAndVisible() self.wdb_makeKeyAndVisible()
theWindow = self theWindow = self
if #available(iOS 15.0, *) {
reconnectVirtualController()
}
if let window = theWindow { if UserDefaults.standard.bool(forKey: "isVirtualController") {
waitforcontroller() 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) method_exchangeImplementations(m1, m2)
} }
} }

View File

@ -7,7 +7,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import SDL2
import GameController import GameController
struct Controller: Identifiable, Hashable { struct Controller: Identifiable, Hashable {
@ -32,13 +31,15 @@ struct iOSNav<Content: View>: View {
class Ryujinx { class Ryujinx {
private var isRunning = false private var isRunning = false
let virtualController = VirtualController()
@Published var controllerMap: [Controller] = [] @Published var controllerMap: [Controller] = []
static let shared = Ryujinx() static let shared = Ryujinx()
private init() {} private init() {}
public struct Configuration : Codable { public struct Configuration : Codable, Equatable {
var gamepath: String var gamepath: String
var inputids: [String] var inputids: [String]
var resscale: Float var resscale: Float
@ -49,7 +50,6 @@ class Ryujinx {
var listinputids: Bool var listinputids: Bool
var fullscreen: Bool var fullscreen: Bool
var memoryManagerMode: String var memoryManagerMode: String
var disableVSync: Bool
var disableShaderCache: Bool var disableShaderCache: Bool
var disableDockedMode: Bool var disableDockedMode: Bool
var enableTextureRecompression: Bool var enableTextureRecompression: Bool
@ -63,7 +63,6 @@ class Ryujinx {
listinputids: Bool = false, listinputids: Bool = false,
fullscreen: Bool = true, fullscreen: Bool = true,
memoryManagerMode: String = "HostMapped", memoryManagerMode: String = "HostMapped",
disableVSync: Bool = false,
disableShaderCache: Bool = false, disableShaderCache: Bool = false,
disableDockedMode: Bool = false, disableDockedMode: Bool = false,
nintendoinput: Bool = true, nintendoinput: Bool = true,
@ -78,7 +77,6 @@ class Ryujinx {
self.tracelogs = tracelogs self.tracelogs = tracelogs
self.listinputids = listinputids self.listinputids = listinputids
self.fullscreen = fullscreen self.fullscreen = fullscreen
self.disableVSync = disableVSync
self.disableShaderCache = disableShaderCache self.disableShaderCache = disableShaderCache
self.disableDockedMode = disableDockedMode self.disableDockedMode = disableDockedMode
self.enableTextureRecompression = enableTextureRecompression self.enableTextureRecompression = enableTextureRecompression
@ -99,7 +97,7 @@ class Ryujinx {
isRunning = true isRunning = true
// Start The Emulation on the main thread // Start The Emulation on the main thread
DispatchQueue.main.async { RunLoop.current.perform {
do { do {
let args = self.buildCommandLineArgs(from: config) let args = self.buildCommandLineArgs(from: config)
@ -145,29 +143,25 @@ class Ryujinx {
args.append("--graphics-backend") args.append("--graphics-backend")
args.append("Vulkan") args.append("Vulkan")
// Fixes the Stubs.DispatchLoop Crash args.append(contentsOf: ["--memory-manager-mode", "SoftwarePageTable"])
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
if config.fullscreen { args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)])
args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)]) args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
args.append(contentsOf: ["--exclusive-fullscreen-width", "1280"]) args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
args.append(contentsOf: ["--exclusive-fullscreen-height", "720"])
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)]) 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 { if config.disableShaderCache {
args.append("--disable-shader-cache") args.append("--disable-shader-cache")
} }
@ -204,9 +198,9 @@ class Ryujinx {
} }
func getConnectedControllers() -> [Controller] { func getConnectedControllers() -> [Controller] {
var nill: String?
guard let jsonPtr = nill else {//get_game_controllers() else { guard let jsonPtr = get_game_controllers() else {
return [] return []
} }

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>MeloID</key>
<string></string>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>
</dict> </dict>

View File

@ -6,12 +6,118 @@
// //
import SwiftUI import SwiftUI
import UIKit
@main @main
struct MeloNXApp: App { 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 { var body: some Scene {
WindowGroup { 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)
}
}

View File

@ -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)
}
}

View File

@ -6,13 +6,16 @@
// //
import SwiftUI import SwiftUI
import SDL2 // import SDL2
import GameController import GameController
import Darwin
import UIKit
import MetalKit
// import SDL
struct MoltenVKSettings: Codable, Hashable { struct MoltenVKSettings: Codable, Hashable {
let string: String let string: String
var bool: Bool? var value: String
var value: String?
} }
struct ContentView: View { struct ContentView: View {
@ -25,38 +28,36 @@ struct ContentView: View {
@State private var config: Ryujinx.Configuration @State private var config: Ryujinx.Configuration
@State private var settings: [MoltenVKSettings] @State private var settings: [MoltenVKSettings]
@State private var isVirtualControllerActive: Bool = false @State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true
@State var onscreencontroller: Controller = Controller(id: "", name: "") @State var onscreencontroller: Controller = Controller(id: "", name: "")
@AppStorage("JIT") var isJITEnabled: Bool = false
// MARK: - Initialization // MARK: - Initialization
init() { init() {
let defaultConfig = Ryujinx.Configuration(gamepath: "") let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
_config = State(initialValue: defaultConfig) _config = State(initialValue: defaultConfig)
let defaultSettings: [MoltenVKSettings] = [ let defaultSettings: [MoltenVKSettings] = [
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "1024"), MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "192"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", value: "1"), MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1") 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) _settings = State(initialValue: defaultSettings)
print("JIT Enabled: \(isJITEnabled)")
initializeSDL() initializeSDL()
} }
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
iOSNav { if let game {
if let game { emulationView
emulationView } else {
} else { mainMenuView
mainMenuView
}
}
.onChange(of: isVirtualControllerActive) { newValue in
if newValue {
createVirtualController()
} else {
destroyVirtualController()
}
} }
} }
@ -69,138 +70,93 @@ struct ContentView: View {
} }
private var mainMenuView: some View { private var mainMenuView: some View {
HStack { MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
GameListView(startemu: $game) .onAppear() {
.onAppear { refreshControllersList()
createVirtualController()
refreshControllersList()
}
settingsListView
}
}
private var settingsListView: some View {
List {
Section("Settings") {
NavigationLink("Config") {
SettingsView(config: $config, MoltenVKSettings: $settings)
.onAppear() {
virtualController?.disconnect()
}
}
} }
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 // MARK: - Helper Methods
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
private func initializeSDL() { private func initializeSDL() {
DispatchQueue.main.async { setMoltenVKSettings()
setMoltenVKSettings() SDL_SetMainReady()
SDL_SetMainReady() SDL_iPhoneSetEventPump(SDL_TRUE)
SDL_iPhoneSetEventPump(SDL_TRUE) SDL_Init(SdlInitFlags)
SDL_Init(SDL_INIT_VIDEO) // initialize()
}
} }
private func setupEmulation() { private func setupEmulation() {
virtualController?.disconnect() virtualController?.disconnect()
patchMakeKeyAndVisible()
if controllersList.first(where: { $0 == onscreencontroller}) != nil { if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
controllerCallback = {
DispatchQueue.main.async {
controllersList = Ryujinx.shared.getConnectedControllers()
print(currentControllers)
start(displayid: 1)
}
}
isVCA = true
showVirtualController()
} else {
DispatchQueue.main.async { DispatchQueue.main.async {
print(currentControllers)
start(displayid: 1) start(displayid: 1)
} }
} else {
isVCA = false
DispatchQueue.main.async {
start(displayid: 1)
}
} }
} }
private func refreshControllersList() { private func refreshControllersList() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
controllersList = Ryujinx.shared.getConnectedControllers()
var controller = controllersList.first(where: { $0.name.hasPrefix("Apple")}) if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
self.onscreencontroller = (controller ?? Controller(id: "", name: "")) self.onscreencontroller = onscreen
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)
}
} }
}
controllersList.removeAll(where: { $0.id == "0"})
private func toggleController(_ controller: Controller) {
if currentControllers.contains(where: { $0.id == controller.id }) { if controllersList.count > 2 {
currentControllers.removeAll(where: { $0.id == controller.id }) let controller = controllersList[2]
} else { currentControllers.append(controller)
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
currentControllers.append(controller) 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) { private func start(displayid: UInt32) {
guard let game else { return } guard let game else { return }
config.gamepath = game.path 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 { do {
try Ryujinx.shared.start(with: config) try Ryujinx.shared.start(with: config)
@ -208,22 +164,9 @@ struct ContentView: View {
print("Error: \(error.localizedDescription)") 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() { private func setMoltenVKSettings() {
if let configs = loadSettings() {
self.config = configs
}
settings.forEach { setting in settings.forEach { setting in
setenv(setting.string, setting.value, 1) setenv(setting.string, setting.value, 1)
@ -245,3 +188,4 @@ func loadSettings() -> Ryujinx.Configuration? {
return nil return nil
} }
} }

View File

@ -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 ""
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -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)
}
}
}
}
}

View File

@ -5,25 +5,147 @@
// Created by Stossy11 on 3/11/2024. // Created by Stossy11 on 3/11/2024.
// //
// MARK: - This will most likely not be used in prod
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
struct GameListView: View {
struct GameLibraryView: View {
@Binding var startemu: URL? @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 { var body: some View {
List(games, id: \.self) { game in iOSNav {
Button { ScrollView {
startemu = game LazyVStack(alignment: .leading, spacing: 20) {
} label: { if !isSearching {
Text(game.lastPathComponent) 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") .background(Color(.systemGroupedBackground))
.onAppear(perform: loadGames) .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() { private func loadGames() {
let fileManager = FileManager.default let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return } 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)") print("Failed to create roms directory: \(error)")
} }
} }
games = []
// Load games only from "roms" folder // Load games only from "roms" folder
do { do {
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil) 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 { } catch {
print("Error loading games from roms folder: \(error)") 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)
}
}

View File

@ -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()
}
}

View File

@ -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) {
}
}

View File

@ -11,6 +11,13 @@ struct SettingsView: View {
@Binding var config: Ryujinx.Configuration @Binding var config: Ryujinx.Configuration
@Binding var MoltenVKSettings: [MoltenVKSettings] @Binding var MoltenVKSettings: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@Binding var onscreencontroller: Controller
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
var memoryManagerModes = [ var memoryManagerModes = [
("HostMapped", "Host (fast)"), ("HostMapped", "Host (fast)"),
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"), ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
@ -18,166 +25,294 @@ struct SettingsView: View {
] ]
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: 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 { var body: some View {
ScrollView { iOSNav {
VStack { List {
Section(header: Title("Graphics and Performance")) { // Graphics & Performance
Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen) Section {
Toggle("Disable V-Sync", isOn: $config.disableVSync) Toggle(isOn: $config.fullscreen) {
Toggle("Disable Shader Cache", isOn: $config.disableShaderCache) labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical")
Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression) }
Toggle("Disable Docked Mode", isOn: $config.disableDockedMode) .tint(.blue)
Resolution(value: $config.resscale)
Toggle("Enable Metal HUD", isOn: $metalHUDEnabled) Toggle(isOn: $config.disableShaderCache) {
.onChange(of: metalHUDEnabled) { newValue in labelWithIcon("Disable Shader Cache", iconName: "memorychip")
if newValue { }
MTLHud.shared.enable() .tint(.blue)
} else {
MTLHud.shared.disable() 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")) { // Input Selector
Toggle("List Input IDs", isOn: $config.listinputids) Section {
Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput) ForEach(controllersList) { controller in
Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo) var customBinding: Binding<Bool> {
// Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory) 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")) { // Input Settings
Toggle("Enable Debug Logs", isOn: $config.debuglogs) Section {
Toggle("Enable Trace Logs", isOn: $config.tracelogs)
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 { // Logging
Spacer() Section {
Picker("Memory Manager Mode", selection: $config.memoryManagerMode) { Toggle(isOn: $config.debuglogs) {
ForEach(memoryManagerModes, id: \.0) { key, displayName in 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) 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.")
} }
// Advanced
Section {
Section(header: Title("Additional Settings")) { DisclosureGroup {
//TextField("Game Path", text: $config.gamepath) HStack {
labelWithIcon("Page Size", iconName: "textformat.size")
Text("PageSize \(String(Int(getpagesize())))") Spacer()
Text("\(String(Int(getpagesize())))")
TextField("Additional Arguments", text: Binding( .foregroundColor(.secondary)
get: {
config.additionalArgs.joined(separator: ", ")
},
set: { newValue in
config.additionalArgs = newValue.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }
} }
))
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() .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
} .navigationTitle("Settings")
.onAppear { .navigationBarTitleDisplayMode(.inline)
if let configs = loadSettings() { .listStyle(.insetGrouped)
self.config = configs .onAppear {
print(configs) if let configs = loadSettings() {
self.config = configs
}
}
.onChange(of: config) { _ in
saveSettings()
} }
} }
.navigationTitle("Settings") .navigationViewStyle(.stack)
.navigationBarItems(trailing: Button("Save") { }
saveSettings()
}) 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() { func saveSettings() {
do { do {
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // Optional: Makes the JSON easier to read encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(config) let data = try encoder.encode(config)
let jsonString = String(data: data, encoding: .utf8) let jsonString = String(data: data, encoding: .utf8)
// Save to UserDefaults
UserDefaults.standard.set(jsonString, forKey: "config") UserDefaults.standard.set(jsonString, forKey: "config")
print("Settings saved successfully!")
} catch { } catch {
print("Failed to save settings: \(error)") print("Failed to save settings: \(error)")
} }
} }
}
// Original loadSettings function assumed to exist
func loadSettings() -> Ryujinx.Configuration? {
struct Resolution: View { guard let jsonString = UserDefaults.standard.string(forKey: "config"),
@Binding var value: Float let data = jsonString.data(using: .utf8) else {
return nil
var body: some View { }
HStack { do {
Text("Resolution Scale (Custom):") let decoder = JSONDecoder()
Spacer() let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
return configs
Button(action: { } catch {
if value > 0.1 { // Prevent values going below 0.1 print("Failed to load settings: \(error)")
value -= 0.10 return nil
value = round(value * 1000) / 1000 // Round to two decimal places
}
print(value)
}) {
Text("-")
.frame(width: 30, height: 30)
.background(Color.gray.opacity(0.2))
.cornerRadius(5)
}
TextField("", value: $value, formatter: NumberFormatter.floatFormatter)
.multilineTextAlignment(.center)
.frame(width: 60)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.decimalPad)
Button(action: {
value += 0.10
value = round(value * 1000) / 1000 // Round to two decimal places
print(value)
}) {
Text("+")
.frame(width: 30, height: 30)
.background(Color.gray.opacity(0.2))
.cornerRadius(5)
}
} }
} }
}
extension NumberFormatter {
static var floatFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
formatter.allowsFloats = true
return formatter
}
}
struct Title: View {
let string: String
init(_ string: String) { @ViewBuilder
self.string = string private func labelWithIcon(_ text: String, iconName: String) -> some View {
} HStack(spacing: 8) {
if !iconName.isEmpty {
var body: some View { Image(systemName: iconName)
VStack { .symbolRenderingMode(.hierarchical)
Text(string) .foregroundStyle(.blue)
.font(.title2) }
Divider() Text(text)
} }
.font(.body)
} }
} }

View File

@ -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")
}
}
}
}

View File

@ -65,7 +65,12 @@ namespace Ryujinx.Common.Configuration
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); 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); string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
// On macOS, check for a portable directory next to the app bundle as well. // On macOS, check for a portable directory next to the app bundle as well.

View File

@ -27,6 +27,7 @@ namespace Ryujinx.Common.SystemInterop
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
private void RegisterPosix() private void RegisterPosix()
{ {
const int StdErrFileno = 2; const int StdErrFileno = 2;
@ -44,6 +45,7 @@ namespace Ryujinx.Common.SystemInterop
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
private async Task EventWorkerAsync(CancellationToken cancellationToken) private async Task EventWorkerAsync(CancellationToken cancellationToken)
{ {
using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true); using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true);
@ -92,6 +94,7 @@ namespace Ryujinx.Common.SystemInterop
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
private static Stream CreateFileDescriptorStream(int fd) private static Stream CreateFileDescriptorStream(int fd)
{ {
return new FileStream( return new FileStream(
@ -100,5 +103,6 @@ namespace Ryujinx.Common.SystemInterop
); );
} }
} }
} }

View File

@ -134,20 +134,12 @@ namespace Ryujinx.Common
private static (Assembly, string) ResolveManifestPath(string filename) 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); var segments = filename.Split('/', 2, StringSplitOptions.RemoveEmptyEntries);
if (segments.Length >= 2) if (segments.Length >= 2)
{ {
foreach (var assembly in System.Runtime.Loader.AssemblyLoadContext.Default.Assemblies) var assembly = Assembly.GetExecutingAssembly();
{ return (assembly, segments[1]);
if (assembly.GetName().Name == segments[0])
{
return (assembly, segments[1]);
}
}
} }
return (_resourceAssembly, filename); return (_resourceAssembly, filename);

View File

@ -88,7 +88,7 @@ namespace Ryujinx.Cpu.Signal
ref SignalHandlerConfig config = ref GetConfigRef(); ref SignalHandlerConfig config = ref GetConfigRef();
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{ {
_signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize)); _signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize));

View File

@ -62,7 +62,7 @@ namespace Ryujinx.Cpu.Signal
throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}"); throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}");
} }
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{ {
result = sigaction(SIGBUS, ref sig, out _); result = sigaction(SIGBUS, ref sig, out _);
@ -77,7 +77,7 @@ namespace Ryujinx.Cpu.Signal
public static bool RestoreExceptionHandler(SigAction oldAction) 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);
} }
} }
} }

View File

@ -7,6 +7,7 @@ using System.Runtime.Versioning;
namespace Ryujinx.Graphics.Vulkan.MoltenVK namespace Ryujinx.Graphics.Vulkan.MoltenVK
{ {
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
public static partial class MVKInitialization public static partial class MVKInitialization
{ {
private const string VulkanLib = "libvulkan.dylib"; private const string VulkanLib = "libvulkan.dylib";

View File

@ -14,6 +14,14 @@ using System.Runtime.InteropServices;
using Format = Ryujinx.Graphics.GAL.Format; using Format = Ryujinx.Graphics.GAL.Format;
using PrimitiveTopology = Ryujinx.Graphics.GAL.PrimitiveTopology; using PrimitiveTopology = Ryujinx.Graphics.GAL.PrimitiveTopology;
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; 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 namespace Ryujinx.Graphics.Vulkan
{ {
@ -498,6 +506,33 @@ namespace Ryujinx.Graphics.Vulkan
Queue = queue; Queue = queue;
QueueLock = new object(); 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); LoadFeatures(maxQueueCount, queueFamilyIndex);
QueueFamilyIndex = queueFamilyIndex; QueueFamilyIndex = queueFamilyIndex;

View File

@ -98,7 +98,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error
SystemLanguage.CanadianFrench => "fr-CA", SystemLanguage.CanadianFrench => "fr-CA",
SystemLanguage.LatinAmericanSpanish => "es-419", SystemLanguage.LatinAmericanSpanish => "es-419",
SystemLanguage.SimplifiedChinese => "zh-Hans", SystemLanguage.SimplifiedChinese => "zh-Hans",
SystemLanguage.TraditionalChinese => "zh-Hant",
SystemLanguage.BrazilianPortuguese => "pt-BR", SystemLanguage.BrazilianPortuguese => "pt-BR",
_ => "en-US", _ => "en-US",
#pragma warning restore IDE0055 #pragma warning restore IDE0055

View File

@ -21,7 +21,6 @@ namespace Ryujinx.HLE.HOS.SystemState
"fr-CA", "fr-CA",
"es-419", "es-419",
"zh-Hans", "zh-Hans",
"zh-Hant",
"pt-BR", "pt-BR",
}; };

View File

@ -102,10 +102,6 @@ namespace Ryujinx.Headless.SDL2
Version = "1"; Version = "1";
// Make process DPI aware for proper window sizing on high-res screens. // Make process DPI aware for proper window sizing on high-res screens.
ForceDpiAware.Windows(); 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; Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS;

View File

@ -48,7 +48,7 @@ namespace Ryujinx.Headless.SDL2
public NpadManager NpadManager { get; } public NpadManager NpadManager { get; }
public TouchScreenManager TouchScreenManager { get; } public TouchScreenManager TouchScreenManager { get; }
public Switch Device { get; private set; } public Switch Device;
public IRenderer Renderer { get; private set; } public IRenderer Renderer { get; private set; }
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;

View File

@ -26,7 +26,6 @@ namespace Ryujinx.Horizon.Sdk.Settings
"fr-CA", "fr-CA",
"es-419", "es-419",
"zh-Hans", "zh-Hans",
"zh-Hant",
"pt-BR" "pt-BR"
}; };

View File

@ -73,7 +73,7 @@ namespace Ryujinx.UI.Common.Helper
/// <returns>A formatted string that can be displayed in the UI.</returns> /// <returns>A formatted string that can be displayed in the UI.</returns>
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
{ {
culture ??= CultureInfo.CurrentCulture; culture ??= CultureInfo.InvariantCulture;
return utcDateTime?.ToLocalTime().ToString(culture); 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> /// <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) 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, // Games that were never played are supposed to appear before the oldest played games in the list,
// so returning DateTime.UnixEpoch here makes sense. // so returning DateTime.UnixEpoch here makes sense.

View File

@ -1154,7 +1154,7 @@ namespace Ryujinx.Ava.UI.ViewModels
return true; return true;
} }
CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo; CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0; return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
} }