Latest Changes and almost works
@ -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);
|
||||
|
@ -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)));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
27
src/MeloNX/MeloNX/Core/DetectJIT/utils.h
Normal 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);
|
91
src/MeloNX/MeloNX/Core/DetectJIT/utils.m
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class MTLHud {
|
||||
|
||||
var canMetalHud: Bool {
|
||||
|
@ -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
|
||||
}
|
||||
|
65
src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 []
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
45
src/MeloNX/MeloNX/Models/Game.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
268
src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift
Normal 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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
27
src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 8.6 KiB |
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.4 KiB |
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 54 KiB |
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
34
src/MeloNX/MeloNX/Views/TabView/TabView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -21,7 +21,6 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
"fr-CA",
|
||||
"es-419",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"pt-BR",
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -26,7 +26,6 @@ namespace Ryujinx.Horizon.Sdk.Settings
|
||||
"fr-CA",
|
||||
"es-419",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"pt-BR"
|
||||
};
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|