Latest Changes and almost works

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

View File

@ -19,8 +19,7 @@ namespace ARMeilleure.CodeGen.Arm64
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
#if __has_feature(modules)
@import UIKit;
@import Foundation;
#else
#import "UIKit/UIKit.h"
#import "Foundation/Foundation.h"
#endif
#define DISPATCH_ASYNC_START dispatch_async(dispatch_get_main_queue(), ^{
#define DISPATCH_ASYNC_CLOSE });
#define PT_TRACE_ME 0
extern int ptrace(int, pid_t, caddr_t, int);
#define CS_DEBUGGED 0x10000000
extern int csops(
pid_t pid,
unsigned int ops,
void *useraddr,
size_t usersize
);
extern BOOL getEntitlementValue(NSString *key);
extern BOOL isJITEnabled(void);
#define DLOG(format, ...) ShowAlert(@"DEBUG", [NSString stringWithFormat:@"\n %s [Line %d] \n %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]])
void ShowAlert(NSString* title, NSString* message, _Bool* showok);

View File

@ -0,0 +1,91 @@
#import "utils.h"
typedef struct __SecTask * SecTaskRef;
extern CFTypeRef SecTaskCopyValueForEntitlement(
SecTaskRef task,
NSString* entitlement,
CFErrorRef _Nullable *error
)
__attribute__((weak_import));
extern SecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator)
__attribute__((weak_import));
BOOL getEntitlementValue(NSString *key)
{
if (SecTaskCreateFromSelf == NULL || SecTaskCopyValueForEntitlement == NULL)
return NO;
SecTaskRef sec_task = SecTaskCreateFromSelf(NULL);
if(!sec_task) return NO;
CFTypeRef value = SecTaskCopyValueForEntitlement(sec_task, key, nil);
if (value != nil)
{
CFRelease(value);
}
CFRelease(sec_task);
return value != nil && [(__bridge id)value boolValue];
}
BOOL isJITEnabled(void)
{
if (getEntitlementValue(@"dynamic-codesigning"))
{
return YES;
}
int flags;
csops(getpid(), 0, &flags, sizeof(flags));
return (flags & CS_DEBUGGED) != 0;
}
void ShowAlert(NSString* title, NSString* message, _Bool* showok)
{
DISPATCH_ASYNC_START
UIWindow* mainWindow = [[UIApplication sharedApplication] windows].lastObject;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
if (showok) {
[alert addAction:[UIAlertAction actionWithTitle:@"ok!"
style:UIAlertActionStyleDefault
handler:nil]];
}
[mainWindow.rootViewController presentViewController:alert
animated:true
completion:nil];
DISPATCH_ASYNC_CLOSE
}
#import <UIKit/UIKit.h>
__attribute__((constructor)) static void entry(int argc, char **argv)
{
if (isJITEnabled()) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"JIT"];
[defaults synchronize]; // Ensure the value is saved immediately
} else {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:NO forKey:@"JIT"];
[defaults synchronize]; // Ensure the value is saved immediately
}
if (getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) {
NSLog(@"Entitlement Does Exist");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"increased-memory-limit"];
[defaults synchronize]; // Ensure the value is saved immediately
}
if (getEntitlementValue(@"com.apple.developer.kernel.increased-debugging-memory-limit")) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"increased-debugging-memory-limit"];
[defaults synchronize]; // Ensure the value is saved immediately
}
if (getEntitlementValue(@"com.apple.developer.kernel.extended-virtual-addressing")) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"extended-virtual-addressing"];
[defaults synchronize]; // Ensure the value is saved immediately
}
}

View File

@ -8,13 +8,29 @@
#ifndef RyujinxHeader
#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

View File

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

View File

@ -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)?
class VirtualController {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
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
]
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))
}
// 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
}
)
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()
}
}
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
}
// 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)
}
}
}
}
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
}
@available(iOS 15.0, *)
func reconnectVirtualController() {
VirtualController.disconnect()
DispatchQueue.main.async {
VirtualController.connect { err in
print("reconnected: err \(String(describing: err))")
}
}
enum ThumbstickType: Int {
case left
case right
}

View File

@ -0,0 +1,65 @@
//
// VirtualController.swift
// MeloNX
//
// Created by Stossy11 on 28/11/2024.
//
import Foundation
import GameController
import UIKit
import SwiftUI
func waitforcontroller() {
if let window = theWindow {
// Function to recursively search for GCControllerView
func findGCControllerView(in view: UIView) -> UIView? {
// Check if current view is GCControllerView
if String(describing: type(of: view)) == "ControllerView" {
return view
}
// Search through subviews
for subview in view.subviews {
if let found = findGCControllerView(in: subview) {
return found
}
}
return nil
}
let controllerView = ControllerView()
let controllerHostingController = UIHostingController(rootView: controllerView)
let containerView = TransparentHostingContainerView(frame: window.bounds)
containerView.backgroundColor = .clear
controllerHostingController.view.frame = containerView.bounds
controllerHostingController.view.backgroundColor = .clear
containerView.addSubview(controllerHostingController.view)
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if findGCControllerView(in: window) == nil {
window.addSubview(containerView)
}
window.bringSubviewToFront(containerView)
}
}
}
class TransparentHostingContainerView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Check if the point is within the subviews of this container
let view = super.hitTest(point, with: event)
// Return nil if the touch is outside visible content (passes through to views below)
return view === self ? nil : view
}
}

View File

@ -19,13 +19,26 @@ extension UIWindow {
}
self.wdb_makeKeyAndVisible()
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)
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
//
// GameInfo.swift
// MeloNX
//
// Created by Stossy11 on 9/12/2024.
//
import SwiftUI
import UniformTypeIdentifiers
public struct Game: Identifiable, Equatable {
public var id = UUID()
var containerFolder: URL
var fileType: UTType
var fileURL: URL
var titleName: String
var titleId: String
var developer: String
var version: String
var icon: UIImage?
func createImage(from gameInfo: GameInfo) -> UIImage? {
// Access the struct
let gameInfoValue = gameInfo
// Get the image data
let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else {
print("Invalid image size.")
return nil
}
// Convert the ImageData byte array to Swift's Data
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
// Create a UIImage (or NSImage on macOS)
print(imageData)
return UIImage(data: imageData)
}
}

View File

@ -6,13 +6,16 @@
//
import SwiftUI
import 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()
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
print(currentControllers)
start(displayid: 1)
}
}
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
}
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 toggleController(_ controller: Controller) {
if currentControllers.contains(where: { $0.id == controller.id }) {
currentControllers.removeAll(where: { $0.id == controller.id })
} else {
currentControllers.append(controller)
}
}
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)
@ -209,21 +165,8 @@ struct ContentView: View {
}
}
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
}
}

View File

@ -0,0 +1,268 @@
//
// ControllerView.swift
// Pomelo-V2
//
// Created by Stossy11 on 16/7/2024.
//
import SwiftUI
import GameController
import SwiftUIJoystick
import CoreMotion
struct ControllerView: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
VStack {
Spacer()
VStack {
HStack {
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
.padding()
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this works
ABXYView()
}
}
.padding()
}
HStack {
ButtonView(button: .start) // Adding the + button
.padding(.horizontal, 40)
ButtonView(button: .back) // Adding the - button
.padding(.horizontal, 40)
}
}
.padding(.bottom, geometry.size.height / 3.2) // very broken
}
} else {
// could be landscape
VStack {
Spacer()
VStack {
HStack {
// gotta fuckin add + and - now
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
HStack {
// Spacer()
VStack {
// Spacer()
ButtonView(button: .back) // Adding the + button
}
Spacer()
VStack {
// Spacer()
ButtonView(button: .start) // Adding the - button
}
// Spacer()
}
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this work s
ABXYView()
}
}
}
}
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
}
}
}
.padding()
}
}
struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
var body: some View {
HStack {
ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder)
.padding(.horizontal)
}
.frame(width: width, height: height)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
}
}
}
struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
var body: some View {
HStack {
ButtonView(button: .rightShoulder)
.padding(.horizontal)
ButtonView(button: .rightTrigger)
.padding(.horizontal)
}
.frame(width: width, height: height)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
}
}
}
struct DPadView: View {
@State var size: CGFloat = 145
var body: some View {
VStack {
ButtonView(button: .dPadUp)
HStack {
ButtonView(button: .dPadLeft)
Spacer(minLength: 20)
ButtonView(button: .dPadRight)
}
ButtonView(button: .dPadDown)
.padding(.horizontal)
}
.frame(width: size, height: size)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
}
}
}
struct ABXYView: View {
@State var size: CGFloat = 145
var body: some View {
VStack {
ButtonView(button: .X)
HStack {
ButtonView(button: .Y)
Spacer(minLength: 20)
ButtonView(button: .A)
}
ButtonView(button: .B)
.padding(.horizontal)
}
.frame(width: size, height: size)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
}
}
}
struct ButtonView: View {
var button: VirtualControllerButton
@State var width: CGFloat = 45
@State var height: CGFloat = 45
@State var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
var body: some View {
Image(systemName: buttonText)
.resizable()
.frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
.opacity(isPressed ? 0.4 : 0.7)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !self.isPressed {
self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy)
}
}
.onEnded { _ in
self.isPressed = false
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
)
.onAppear() {
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
width = 65
}
if button == .back || button == .start || button == .guide {
width = 35
height = 35
}
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
}
}
private var buttonText: String {
switch button {
case .A:
return "a.circle.fill"
case .B:
return "b.circle.fill"
case .X:
return "x.circle.fill"
case .Y:
return "y.circle.fill"
case .dPadUp:
return "arrowtriangle.up.circle.fill"
case .dPadDown:
return "arrowtriangle.down.circle.fill"
case .dPadLeft:
return "arrowtriangle.left.circle.fill"
case .dPadRight:
return "arrowtriangle.right.circle.fill"
case .leftTrigger:
return"zl.rectangle.roundedtop.fill"
case .rightTrigger:
return "zr.rectangle.roundedtop.fill"
case .leftShoulder:
return "l.rectangle.roundedbottom.fill"
case .rightShoulder:
return "r.rectangle.roundedbottom.fill"
case .start:
return "plus.circle.fill" // System symbol for +
case .back:
return "minus.circle.fill" // System symbol for -
case .guide:
return "house.circle.fill"
// This should be all the cases
default:
return ""
}
}
}

View File

@ -0,0 +1,27 @@
//
// Haptics.swift
// Pomelo
//
// Created by Stossy11 on 11/9/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import UIKit
import SwiftUI
class Haptics {
static let shared = Haptics()
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
}
}

View File

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

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "JoyStickBase@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "JoyStickBase@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "JoyStickBase@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "JoyStickHandle@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "JoyStickHandle@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "JoyStickHandle@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "JoyStickBaseCustom@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "JoyStickBaseCustom@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "JoyStickBaseCustom@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "JoyStickHandleCustom@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "JoyStickHandleCustom@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "JoyStickHandleCustom@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,53 @@
//
// JoystickView.swift
// Pomelo
//
// Created by Stossy11 on 30/9/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import SwiftUI
import SwiftUIJoystick
public struct Joystick: View {
@State var iscool: Bool? = nil
@ObservedObject public var joystickMonitor = JoystickMonitor()
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
private let shape: JoystickShape = .circle
public var body: some View {
VStack{
JoystickBuilder(
monitor: self.joystickMonitor,
width: self.dragDiameter,
shape: .circle,
background: {
Text("")
.hidden()
},
foreground: {
Circle().fill(Color.gray)
.opacity(0.7)
},
locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
let scaledX = Float(newValue.x)
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
print("Joystick Position: (\(scaledX), \(scaledY))")
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
}
}
}
}
}

View File

@ -5,23 +5,145 @@
// 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() {
@ -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)
}
}

View File

@ -1,84 +0,0 @@
//
// VulkanSDLView.swift
// MeloNX
//
// Created by Stossy11 on 3/11/2024.
//
import UIKit
import MetalKit
import SDL2
class SDLView: UIView {
var sdlwin: OpaquePointer?
override init(frame: CGRect) {
super.init(frame: frame)
DispatchQueue.main.async { [self] in
makeSDLWindow()
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
DispatchQueue.main.async { [self] in
makeSDLWindow()
}
}
func getWindowFlags() -> UInt32 {
return SDL_WINDOW_VULKAN.rawValue
}
private func makeSDLWindow() {
let width: Int32 = 1280 // Replace with the desired width
let height: Int32 = 720 // Replace with the desired height
let defaultFlags: UInt32 = SDL_WINDOW_SHOWN.rawValue
let fullscreenFlag: UInt32 = SDL_WINDOW_FULLSCREEN.rawValue // Or SDL_WINDOW_FULLSCREEN_DESKTOP if needed
// Create the SDL window
sdlwin = SDL_CreateWindow(
"Ryujinx",
0,
0,
width,
height,
defaultFlags | getWindowFlags() // | fullscreenFlag | getWindowFlags()
)
// Check if we successfully retrieved the SDL window
guard sdlwin != nil else {
print("Error creating SDL window: \(String(cString: SDL_GetError()))")
return
}
print("SDL window created successfully.")
// Position SDL window over this UIView
self.syncSDLWindowPosition()
}
private func syncSDLWindowPosition() {
guard let sdlwin = sdlwin else { return }
// Get the frame of the UIView in screen coordinates
let viewFrameInWindow = self.convert(self.bounds, to: nil)
// Set the SDL window position and size to match the UIView frame
SDL_SetWindowPosition(sdlwin, Int32(viewFrameInWindow.origin.x), Int32(viewFrameInWindow.origin.y))
SDL_SetWindowSize(sdlwin, Int32(viewFrameInWindow.width), Int32(viewFrameInWindow.height))
// Bring SDL window to the front
SDL_RaiseWindow(sdlwin)
print("SDL window positioned over SDLView.")
}
override func layoutSubviews() {
super.layoutSubviews()
// Adjust SDL window whenever layout changes
syncSDLWindowPosition()
}
}

View File

@ -1,27 +0,0 @@
//
// VulkanSDLViewRepresentable.swift
// MeloNX
//
// Created by Stossy11 on 3/11/2024.
//
import UIKit
import SwiftUI
import SDL2
import GameController
struct SDLViewRepresentable: UIViewRepresentable {
let configure: (UInt32) -> Void
func makeUIView(context: Context) -> SDLView {
// Configure (start ryu) before initialsing SDLView so SDLView can get the SDL_Window from Ryu
let view = SDLView(frame: .zero)
configure(SDL_GetWindowID(view.sdlwin))
return view
}
func updateUIView(_ uiView: SDLView, context: Context) {
}
}

View File

@ -11,6 +11,13 @@ struct SettingsView: View {
@Binding var config: Ryujinx.Configuration
@Binding var 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)
}
}

View File

@ -0,0 +1,34 @@
//
// TabView.swift
// MeloNX
//
// Created by Stossy11 on 10/12/2024.
//
import SwiftUI
import UniformTypeIdentifiers
struct MainTabView: View {
@Binding var startemu: URL?
@Binding var config: Ryujinx.Configuration
@Binding var MVKconfig: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@Binding var onscreencontroller: Controller
var body: some View {
TabView {
GameLibraryView(startemu: $startemu)
.tabItem {
Label("Games", systemImage: "gamecontroller.fill")
}
SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.tabItem {
Label("Settings", systemImage: "gear")
}
}
}
}

View File

@ -65,7 +65,12 @@ namespace Ryujinx.Common.Configuration
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ namespace Ryujinx.UI.Common.Helper
/// <returns>A formatted string that can be displayed in the UI.</returns>
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.

View File

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