1
0
forked from MeloNX/MeloNX

Compare commits

..

16 Commits

35 changed files with 1457 additions and 682 deletions

View File

@ -6,11 +6,6 @@
<h1 align="center">MeloNX</h1> <h1 align="center">MeloNX</h1>
## Documentation
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
<p align="center"> <p align="center">
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base. MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
</p> </p>
@ -24,15 +19,20 @@ If you are planning to contribute or just want to learn more about this project
MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>. MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>.
# Change
Modified the button layout in landscape mode and added a new joystick designed for FTG.
Tested only on iPhone 13 Pro Max running iOS 16.6.1.
Modified by PhoenixR.
# Usage # Usage
## FAQ ## FAQ
- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all. - MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
- MeloNX needs Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue) - MeloNX cannot be Sideloaded normally and requires the use of the following Installation Guide(s).
- MeloNX needs JIT - MeloNX requires JIT
- Recommended Device: iPhone 15 Pro or newer. - Recommended Device: iPhone 15 Pro or newer.
- Low-End Recommended Device**: iPhone 13 Pro. - Low-End Recommended Device: iPhone 13 Pro.
- Lowest Supported Device: iPhone XR
## How to install ## How to install
@ -54,14 +54,67 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
4. **Enable JIT** 4. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation. - Use your preferred method to enable Just-In-Time (JIT) compilation.
- We reccomend using [JitStreamer](https://jkcoxson.com/jitstreamer)
5. **Add Necessary Files** 5. **Add Necessary Files**
If having Issues installing firmware (Make sure your Keys are installed first) If having Issues installing firmware (Make sure your Keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop**. - If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders - Copy the **bis** and **system** folders
### Xcode ### Free Developer Account (Experimental)
1. **Sideload MeloNX**
- Use [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
2. **Sideload the Entitlement App**
- Install [this app](https://github.com/hugeBlack/GetMoreRam/releases/download/nightly/Entitlement.ipa) using [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
3. **Sign In to Your Account**
- Open **Settings** in the entitlement app and sign in with your Apple ID.
4. **Refresh App IDs**
- Navigate to the **App IDs** page.
- Tap **Refresh** to update the list.
5. **Enable Increased Memory Limit**
- Select **MeloNX** (should be like "com.stossy11.MeloNX" or some variation) from the list.
- Tap **Add Increased Memory Limit**.
6. **Reinstall MeloNX**
- Delete the existing installation.
- Sideload the app again using SideStore or AltStore.
7. **Verify Increased Memory Limit**
- Open MeloNX and check if the **Increased Memory Limit** is enabled.
8. **Add Necessary Files**
If having Issues installing firmware (Make sure your keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
9. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- We recommend using [JitStreamer](https://jkcoxson.com/jitstreamer)
### TrollStore
As Said in FAQ:
> MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
1. **Install MeloNX with TrollStore**
2. **Add Necessary Files**
3. **Enable TrollStore JIT**
- MeloNX includes automatic JIT using the TrollStore URL Scheme
- Open MeloNX Settings
- Scroll down and enable the "TrollStore JIT" toggle
- Profit
### Free Developer Account (Xcode)
**NOTE: These Xcode builds are nightly and may have unfinished features.** **NOTE: These Xcode builds are nightly and may have unfinished features.**
@ -70,8 +123,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
2. **Add Necessary Files** 2. **Add Necessary Files**
If having Issues installing firmware (Make sure your Keys are installed first) If having Issues installing firmware (Make sure your keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop**. - If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders - Copy the **bis** and **system** folders
## Features ## Features
@ -99,7 +152,7 @@ If having Issues installing firmware (Make sure your Keys are installed first)
- **Input** - **Input**
We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers. We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
Motion controls are not natively supported for now. Motion controls are natively supported in most cases.
- **DLC & Modifications** - **DLC & Modifications**

View File

@ -8,6 +8,6 @@
// Configuration settings file format documentation can be found at: // Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
VERSION = 1.5.0 VERSION = 1.6.0
DOTNET = /usr/local/share/dotnet/dotnet DOTNET = /usr/local/share/dotnet/dotnet

View File

@ -24,7 +24,6 @@
/* End PBXAggregateTarget section */ /* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
256C91642D8126E300F9736D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 256C91632D8126E300F9736D /* Alamofire */; };
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; }; 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; }; 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
@ -33,6 +32,13 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = { 4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */; containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -47,13 +53,6 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585; remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX; remoteInfo = MeloNX;
}; };
4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = { BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */; containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -206,7 +205,6 @@
files = ( files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */, 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
256C91642D8126E300F9736D /* Alamofire in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
); );
@ -296,7 +294,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */, 4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */, 4E80A98F2CD6F54500029585 /* MeloNX */,
@ -305,7 +303,6 @@
packageProductDependencies = ( packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */, 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */,
256C91632D8126E300F9736D /* Alamofire */,
); );
productName = MeloNX; productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */; productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -365,7 +362,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620; LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1610; LastUpgradeCheck = 1620;
TargetAttributes = { TargetAttributes = {
4E80A98C2CD6F54500029585 = { 4E80A98C2CD6F54500029585 = {
CreatedOnToolsVersion = 16.1; CreatedOnToolsVersion = 16.1;
@ -398,7 +395,6 @@
packageReferences = ( packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */, 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */,
); );
preferredProjectObjectVersion = 56; preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */; productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -457,7 +453,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/native/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n"; shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/publish/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@ -486,6 +482,11 @@
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
};
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = { 4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
@ -496,11 +497,6 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */; targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
}; };
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */;
};
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = { BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */; target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@ -655,7 +651,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 4TD3JXVDW7; DEVELOPMENT_TEAM = 95J8WZ4TN8;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -717,7 +713,7 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES; INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@ -840,12 +836,15 @@
"$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@ -864,7 +863,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 4TD3JXVDW7; DEVELOPMENT_TEAM = 95J8WZ4TN8;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -926,7 +925,7 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES; INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@ -1049,12 +1048,15 @@
"$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@ -1251,14 +1253,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = { 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick"; repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
@ -1278,11 +1272,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
256C91632D8126E300F9736D /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = 256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = { 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */; package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;

View File

@ -1,15 +1,6 @@
{ {
"originHash" : "587a0e7c5c7d612a2c16a973e66df9a6a582b963cb51df7c89fd96cb28ef4a63", "originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
"pins" : [ "pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{ {
"identity" : "swiftsvg", "identity" : "swiftsvg",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A99C2CD6F54700029585"
BuildableName = "MeloNXTests.xctest"
BlueprintName = "MeloNXTests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A9A62CD6F54700029585"
BuildableName = "MeloNXUITests.xctest"
BlueprintName = "MeloNXUITests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Debug"
revealArchiveInOrganizer = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
</ActionContent>
</ExecutionAction>
</PreActions>
</ArchiveAction>
</Scheme>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1610" LastUpgradeVersion = "1620"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@ -64,7 +64,6 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
@ -105,8 +104,17 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent <ActionContent
title = "Run Script" title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;" scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;echo &quot;hi&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash"> shellToInvoke = "/bin/bash">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent> </ActionContent>
</ExecutionAction> </ExecutionAction>
</PreActions> </PreActions>

View File

@ -11,7 +11,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent <ActionContent
title = "Run Script" title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/get_dotnet.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;" scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/get_dotnet.sh&quot;&#10;&#10;echo &quot;Xcode is located at: $DEVELOPER_DIR&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash"> shellToInvoke = "/bin/bash">
<EnvironmentBuildable> <EnvironmentBuildable>
<BuildableReference <BuildableReference

View File

@ -19,12 +19,13 @@ func enableJITEB() {
return return
} }
guard let httpResponse = response as? HTTPURLResponse else {
return
}
DispatchQueue.main.async { DispatchQueue.main.async {
showLaunchAppAlert(jsonData: data!, in: UIApplication.shared.windows.last!.rootViewController!) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let lastWindow = windowScene.windows.last {
showLaunchAppAlert(jsonData: data!, in: lastWindow.rootViewController!)
} else {
fatalError("Unable to get Window")
}
} }
return return

View File

@ -78,8 +78,6 @@ class NativeController: Hashable {
return return
} }
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID)) controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil { if controller == nil {

View File

@ -70,8 +70,6 @@ class VirtualController {
return return
} }
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID)) controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil { if controller == nil {

View File

@ -10,9 +10,7 @@ import Foundation
class MTLHud { class MTLHud {
var canMetalHud: Bool { @Published var canMetalHud: Bool = false
return openMetalDylib()
}
var isEnabled: Bool { var isEnabled: Bool {
if let getenv = getenv("MTL_HUD_ENABLED") { if let getenv = getenv("MTL_HUD_ENABLED") {
@ -24,7 +22,17 @@ class MTLHud {
static let shared = MTLHud() static let shared = MTLHud()
private init() { private init() {
openMetalDylib() let _ = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer,
https://youtu.be/tc65SNOTMz4 7:23)
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
enable()
} else {
disable()
}
}
func toggle() {
print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") { if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
enable() enable()
} else { } else {
@ -35,16 +43,15 @@ class MTLHud {
func openMetalDylib() -> Bool { func openMetalDylib() -> Bool {
let path = "/usr/lib/libMTLHud.dylib" let path = "/usr/lib/libMTLHud.dylib"
// Load the dynamic library
if dlopen(path, RTLD_NOW) != nil { if dlopen(path, RTLD_NOW) != nil {
// Library loaded successfully
print("Library loaded from \(path)") print("Library loaded from \(path)")
canMetalHud = true
return true return true
} else { } else {
// Handle error
if let error = String(validatingUTF8: dlerror()) { if let error = String(validatingUTF8: dlerror()) {
print("Error loading library: \(error)") print("Error loading library: \(error)")
} }
canMetalHud = false
return false return false
} }
} }

View File

@ -360,7 +360,6 @@ class Ryujinx {
} }
func fetchFirmwareVersion() -> String { func fetchFirmwareVersion() -> String {
do {
let firmwareVersionPointer = installed_firmware_version() let firmwareVersionPointer = installed_firmware_version()
if let pointer = firmwareVersionPointer { if let pointer = firmwareVersionPointer {
let firmwareVersion = String(cString: pointer) let firmwareVersion = String(cString: pointer)
@ -370,10 +369,6 @@ class Ryujinx {
return firmwareVersion return firmwareVersion
} }
} catch {
print(error)
}
return "0" return "0"
} }
@ -501,62 +496,6 @@ class Ryujinx {
} }
func repeatuntilfindLayer() {
Task { @MainActor in
while self.metalLayer == nil {
let layer = self.getMetalLayer(nil)
if layer != nil {
self.metalLayer = layer
break
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
@MainActor
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
var window = window
if window == nil {
window = SDL_GetWindowFromID(1)
}
var windowInfo = SDL_SysWMinfo()
SDL_GetWindowWMInfo(window, &windowInfo)
guard let uiWindow = windowInfo.info.uikit.window,
let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else {
print("Unable to get root view")
return nil
}
func findMetalLayer(in view: UIView) -> CAMetalLayer? {
if let metalLayer = view.layer as? CAMetalLayer {
return metalLayer
}
for subview in view.subviews {
if let metalLayer = findMetalLayer(in: subview) {
return metalLayer
}
}
return nil
}
if let existingLayer = findMetalLayer(in: rootView) {
print("Found Metal Layer")
return existingLayer
}
print("found nothing")
return nil
}
static func log(_ message: String) { static func log(_ message: String) {
print("[Ryujinx] \(message)") print("[Ryujinx] \(message)")

View File

@ -32,7 +32,7 @@ struct LaunchGameIntentDef: AppIntent {
let ryujinx = Ryujinx.shared.games let ryujinx = Ryujinx.shared.games
let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName)) let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
let urlString = "melonx://game?name=\(name ?? gameName)" let urlString = "melonx://game?name=\(name ?? gameName)"
print(urlString) print(urlString)

View File

@ -63,22 +63,15 @@ public struct Game: Identifiable, Equatable, Hashable {
} }
func createImage(from gameInfo: GameInfo) -> UIImage? { func createImage(from gameInfo: GameInfo) -> UIImage? {
// Access the struct
let gameInfoValue = gameInfo let gameInfoValue = gameInfo
// Get the image data
let imageSize = Int(gameInfoValue.ImageSize) let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else { guard imageSize > 0, imageSize <= 1024 * 1024 else {
print("Invalid image size.") print("Invalid image size.")
return nil return nil
} }
// Convert the ImageData byte array to Swift's Data
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize) let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
// Create a UIImage (or NSImage on macOS)
print(imageData)
return UIImage(data: imageData) return UIImage(data: imageData)
} }
} }

View File

@ -5,6 +5,7 @@
// Created by Bella on 12/03/2025. // Created by Bella on 12/03/2025.
// //
struct LatestVersionResponse: Codable { struct LatestVersionResponse: Codable {
let version_number: String let version_number: String
let version_number_stripped: String let version_number_stripped: String

View File

@ -6,12 +6,10 @@
// //
import SwiftUI import SwiftUI
// import SDL2
import GameController import GameController
import Darwin import Darwin
import UIKit import UIKit
import MetalKit import MetalKit
// import SDL
struct MoltenVKSettings: Codable, Hashable { struct MoltenVKSettings: Codable, Hashable {
let string: String let string: String
@ -19,6 +17,8 @@ struct MoltenVKSettings: Codable, Hashable {
} }
struct ContentView: View { struct ContentView: View {
// MARK: - Properties
// Games // Games
@State private var game: Game? @State private var game: Game?
@ -55,26 +55,25 @@ struct ContentView: View {
@State var isLoading = true @State var isLoading = true
@State var jitNotEnabled = false @State var jitNotEnabled = false
// MARK: - SDL
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
// MARK: - Initialization // MARK: - Initialization
init() { init() {
var defaultConfig = loadSettings() var defaultConfig = loadSettings()
if defaultConfig == nil { if defaultConfig == nil {
saveSettings(config: .init(gamepath: "")) saveSettings(config: .init(gamepath: ""))
defaultConfig = loadSettings() defaultConfig = loadSettings()
} }
_config = State(initialValue: defaultConfig!) _config = State(initialValue: defaultConfig!)
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings. let defaultSettings: [MoltenVKSettings] = [
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"), MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"), MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_DEBUG", value: "0"), MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"), MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"), MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"), MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
] ]
@ -85,8 +84,18 @@ struct ContentView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
if game != nil, !jitNotEnabled { if game != nil && (!jitNotEnabled || ignoreJIT) {
// This is when the game starts to stop the animation gameView
} else if game != nil && jitNotEnabled {
jitErrorView
} else {
mainMenuView
}
}
// MARK: - View Components
private var gameView: some View {
ZStack { ZStack {
if #available(iOS 16, *) { if #available(iOS 16, *) {
EmulationView(startgame: $game) EmulationView(startgame: $game)
@ -97,36 +106,16 @@ struct ContentView: View {
if isLoading { if isLoading {
ZStack { ZStack {
Color.black Color.black.opacity(0.8)
.opacity(0.8) emulationView.ignoresSafeArea(.all)
emulationView
.ignoresSafeArea(.all)
} }
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all) .ignoresSafeArea(.all)
} }
} }
} else if game != nil, ignoreJIT {
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
} }
if isLoading { private var jitErrorView: some View {
ZStack {
Color.black
.opacity(0.8)
emulationView
.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
} else if game != nil {
Text("") Text("")
.sheet(isPresented: $jitNotEnabled) { .sheet(isPresented: $jitNotEnabled) {
JITPopover() { JITPopover() {
@ -134,64 +123,76 @@ struct ContentView: View {
} }
.interactiveDismissDisabled() .interactiveDismissDisabled()
} }
} else { }
// This is the main menu view that includes the Settings and the Game Selector
mainMenuView private var mainMenuView: some View {
.onAppear() { MainTabView(
startemu: $game,
config: $config,
MVKconfig: $settings,
controllersList: $controllersList,
currentControllers: $currentControllers,
onscreencontroller: $onscreencontroller
)
.onAppear {
quits = false quits = false
let _ = loadSettings()
loadSettings()
isLoading = true isLoading = true
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts. Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
}
.onOpenURL() { url in
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
}
}
}
}
}
private func initControllerObservers() {
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList() refreshControllersList()
} }
}
NotificationCenter.default.addObserver( print(MTLHud.shared.isEnabled)
forName: .GCControllerDidDisconnect,
object: nil, initControllerObservers()
queue: .main) { notification in
if let controller = notification.object as? GCController { Air.play(AnyView(
print("Controller disconnected: \(controller.productCategory)") VStack {
nativeControllers[controller]?.cleanup() Image(systemName: "gamecontroller")
nativeControllers[controller] = nil .font(.system(size: 300))
refreshControllersList() .foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
} }
))
checkJitStatus()
}
.onOpenURL { url in
handleDeepLink(url)
} }
} }
// MARK: - View Components
private var emulationView: some View { private var emulationView: some View {
GeometryReader { screenGeometry in GeometryReader { screenGeometry in
ZStack { ZStack {
gameLoadingContent(screenGeometry: screenGeometry)
HStack{
VStack {
if showlogsloading {
LogFileView(isfps: true)
.frame(alignment: .topLeading)
}
Spacer()
}
Spacer()
}
}
}
}
// MARK: - Helper Methods
private func gameLoadingContent(screenGeometry: GeometryProxy) -> some View {
HStack(spacing: screenGeometry.size.width * 0.04) { HStack(spacing: screenGeometry.size.width * 0.04) {
if let icon = game?.icon { if let icon = game?.icon {
Image(uiImage: icon) Image(uiImage: icon)
@ -209,6 +210,18 @@ struct ContentView: View {
.font(.system(size: min(screenGeometry.size.width * 0.04, 32))) .font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
.foregroundColor(.white) .foregroundColor(.white)
loadingProgressBar(screenGeometry: screenGeometry)
}
}
.padding(.horizontal, screenGeometry.size.width * 0.06)
.padding(.vertical, screenGeometry.size.height * 0.05)
.position(
x: screenGeometry.size.width / 2,
y: screenGeometry.size.height * 0.5
)
}
private func loadingProgressBar(screenGeometry: GeometryProxy) -> some View {
GeometryReader { geometry in GeometryReader { geometry in
let containerWidth = min(screenGeometry.size.width * 0.35, 350) let containerWidth = min(screenGeometry.size.width * 0.35, 350)
@ -234,20 +247,14 @@ struct ContentView: View {
.clipShape(RoundedRectangle(cornerRadius: 16)) .clipShape(RoundedRectangle(cornerRadius: 16))
.onAppear { .onAppear {
isAnimating = true isAnimating = true
setupEmulation() setupEmulation()
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if get_current_fps() != 0 { if get_current_fps() != 0 {
withAnimation { withAnimation {
isLoading = false isLoading = false
isAnimating = false isAnimating = false
} }
timer.invalidate() timer.invalidate()
} }
} }
@ -256,59 +263,42 @@ struct ContentView: View {
.frame(height: min(screenGeometry.size.height * 0.015, 12)) .frame(height: min(screenGeometry.size.height * 0.015, 12))
.frame(width: min(screenGeometry.size.width * 0.35, 350)) .frame(width: min(screenGeometry.size.width * 0.35, 350))
} }
}
.padding(.horizontal, screenGeometry.size.width * 0.06)
.padding(.vertical, screenGeometry.size.height * 0.05)
.position(
x: screenGeometry.size.width / 2,
y: screenGeometry.size.height * 0.5
)
}
if showlogsloading {
LogFileView(isfps: true)
.frame(alignment: .topLeading)
}
}
}
private var mainMenuView: some View {
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
refreshControllersList()
}
Air.play(AnyView(
VStack {
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
))
jitNotEnabled = !isJITEnabled()
if jitNotEnabled {
useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT")
}
}
}
// MARK: - Helper Methods
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
private func initializeSDL() { private func initializeSDL() {
setMoltenVKSettings() setMoltenVKSettings()
SDL_SetMainReady() // Sets SDL Ready SDL_SetMainReady()
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true SDL_iPhoneSetEventPump(SDL_TRUE)
SDL_Init(SdlInitFlags) // Initialises SDL2 SDL_Init(sdlInitFlags)
initialize() initialize()
} }
private func initControllerObservers() {
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main
) { notification in
if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
}
}
}
private func setupEmulation() { private func setupEmulation() {
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
@ -330,8 +320,7 @@ struct ContentView: View {
currentControllers = [] currentControllers = []
if controllersList.count == 1 { if controllersList.count == 1 {
let controller = controllersList[0] currentControllers.append(controllersList[0])
currentControllers.append(controller)
} else if (controllersList.count - 1) >= 1 { } else if (controllersList.count - 1) >= 1 {
for controller in controllersList { for controller in controllersList {
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) { if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
@ -341,29 +330,18 @@ struct ContentView: View {
} }
} }
private func start(displayid: UInt32) { private func start(displayid: UInt32) {
guard let game else { return } guard let game else { return }
config.gamepath = game.fileURL.path config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id))) config.inputids = Array(Set(currentControllers.map(\.id)))
if mVKPreFillBuffer { configureEnvironmentVariables()
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
setenv(setting.string, setting.value, 1)
}
if syncqsubmits {
let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2")
setenv(setting.string, setting.value, 1)
}
if config.inputids.isEmpty { if config.inputids.isEmpty {
config.inputids.append("0") config.inputids.append("0")
} }
do { do {
try Ryujinx.shared.start(with: config) try Ryujinx.shared.start(with: config)
} catch { } catch {
@ -371,14 +349,45 @@ struct ContentView: View {
} }
} }
private func configureEnvironmentVariables() {
if mVKPreFillBuffer {
setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
}
if syncqsubmits {
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1)
}
}
// Sets MoltenVK Environment Variables
private func setMoltenVKSettings() { private func setMoltenVKSettings() {
settings.forEach { setting in settings.forEach { setting in
setenv(setting.string, setting.value, 1) setenv(setting.string, setting.value, 1)
} }
} }
private func checkJitStatus() {
jitNotEnabled = !isJITEnabled()
if jitNotEnabled {
if useTrollStore {
askForJIT()
} else if jitStreamerEB {
enableJITEB()
} else {
print("no JIT")
}
}
}
private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
}
}
}
} }
extension Array { extension Array {

View File

@ -30,7 +30,7 @@ struct ControllerView: View {
VStack { VStack {
ShoulderButtonsViewRight() ShoulderButtonsViewRight()
ZStack { ZStack {
Joystick(iscool: true) // hope this works Joystick(iscool: true) // hope this works JoyStick
ABXYView() ABXYView()
} }
} }
@ -47,7 +47,7 @@ struct ControllerView: View {
} else { } else {
// could be landscape // could be landscape
VStack { /*VStack {
Spacer() Spacer()
VStack { VStack {
@ -57,8 +57,9 @@ struct ControllerView: View {
VStack { VStack {
ShoulderButtonsViewLeft() ShoulderButtonsViewLeft()
ZStack { ZStack {
Joystick() //Joystick()
DPadView() Joystick4FTG()
//DPadView() Disable landscape Dpad
} }
} }
HStack { HStack {
@ -77,7 +78,7 @@ struct ControllerView: View {
VStack { VStack {
ShoulderButtonsViewRight() ShoulderButtonsViewRight()
ZStack { ZStack {
Joystick(iscool: true) // hope this work s //Joystick(iscool: true) // hope this work Disable right Joystick
ABXYView() ABXYView()
} }
} }
@ -85,6 +86,35 @@ struct ControllerView: View {
} }
// .padding(.bottom, geometry.size.height / 11) // also extremally broken ( // .padding(.bottom, geometry.size.height / 11) // also extremally broken (
}*/
VStack {
Spacer() //
HStack(alignment: .bottom) {
VStack(alignment: .leading) {
//ShoulderButtonsViewLeft()
Joystick4FTG() //
//.frame(maxWidth: .infinity, alignment: .leading)
}
.offset(x: 10, y: -5) // +
HStack {
VStack {
ButtonView(button: .back) // -
}
Spacer()
VStack {
ButtonView(button: .start) // +
}
}
VStack {
ShoulderButtonsViewRight()
ZStack {
ABXYView()
}
}.offset(x: 30, y: 25) // 50 25
}
.padding(.bottom, 10) // padding
} }
} }
} }
@ -99,10 +129,11 @@ struct ShoulderButtonsViewLeft: View {
var body: some View { var body: some View {
HStack { HStack {
ButtonView(button: .leftTrigger) //move left Trigger
.padding(.horizontal) // ButtonView(button: .leftTrigger)
ButtonView(button: .leftShoulder) // .padding(.horizontal)
.padding(.horizontal) //ButtonView(button: .leftShoulder)
// .padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear() {
@ -118,8 +149,8 @@ struct ShoulderButtonsViewLeft: View {
} }
struct ShoulderButtonsViewRight: View { struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160 @State var width: CGFloat = 100
@State var height: CGFloat = 20 @State var height: CGFloat = 60
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
@ -128,6 +159,15 @@ struct ShoulderButtonsViewRight: View {
.padding(.horizontal) .padding(.horizontal)
ButtonView(button: .rightTrigger) ButtonView(button: .rightTrigger)
.padding(.horizontal) .padding(.horizontal)
}
HStack{
//put left Trigger there
ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder)
.padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear() {
@ -171,7 +211,7 @@ struct ABXYView: View {
@State var size: CGFloat = 145 @State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { /*var body: some View {
VStack { VStack {
ButtonView(button: .X) ButtonView(button: .X)
HStack { HStack {
@ -182,6 +222,21 @@ struct ABXYView: View {
ButtonView(button: .B) ButtonView(button: .B)
.padding(.horizontal) .padding(.horizontal)
} }
*/
//
var body: some View {
VStack(spacing: 5){
HStack(spacing: 5) {
ButtonView(button: .Y)
ButtonView(button: .B)
}
HStack(spacing: 5) {
ButtonView(button: .X)
ButtonView(button: .A)
}
}
.frame(width: size, height: size) .frame(width: size, height: size)
.onAppear() { .onAppear() {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
@ -195,8 +250,8 @@ struct ABXYView: View {
struct ButtonView: View { struct ButtonView: View {
var button: VirtualControllerButton var button: VirtualControllerButton
@State var width: CGFloat = 45 @State var width: CGFloat = 90
@State var height: CGFloat = 45 @State var height: CGFloat = 90
@State var isPressed = false @State var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ -210,14 +265,14 @@ struct ButtonView: View {
.resizable() .resizable()
.frame(width: width, height: height) .frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) .foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
.opacity(isPressed ? 0.4 : 0.7) .opacity(isPressed ? 0.3 : 0.6)
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in
if !self.isPressed { if !self.isPressed {
self.isPressed = true self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button) Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy) Haptics.shared.play(.light)
} }
} }
.onEnded { _ in .onEnded { _ in

View File

@ -0,0 +1,592 @@
//
// Joystick4FTG.swift
// MeloNX
//
// Created by RZH on 2025/3/7.
//
import SwiftUI
public struct Joystick4FTG: View {
//
@State var iscool: Bool? = nil
@State private var joystickPositionText = ""
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
public var body: some View {
VStack{
//
//FTGJoystick
/*FTGJoystickBuilder { relativePosition in
let scaledX = Double(relativePosition.x )
let scaledY = Double(relativePosition.y )
//print("Joystick Position: (\(scaledX), \(scaledY))")
*/
//
/*
EnhancedEightWayJoystick{relativePosition in
let scaledX = Double(relativePosition.x * 10)
let scaledY = Double(relativePosition.y * 10)
let formattedText = String(format: "X: %5.2f, Y: %5.2f", scaledX, scaledY)
print("Joystick Position: \(formattedText)")
//
//self.joystickPositionText = formattedText
*/
/*
//
FreeRoamJoystick { position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
//print("Joystick Position: \(formattedText)")
//
//self.joystickPositionText = formattedText
*/
//
/*
NineGridJoystick { position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
*/
//
DynamicNineGridJoystick{ position in
let scaledX = Double(position.x * 10)
let scaledY = Double(position.y * 10)
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: scaledX, y: scaledY)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: scaledX, y: scaledY)
}
}
}
}
}
struct EnhancedEightWayJoystick: View {
var onDirectionChanged: ((x: Int, y: Int)) -> Void
@State private var currentDirection: (x: Int, y: Int) = (0, 0)
private let radius: CGFloat = 50
private let centerPoint = CGPoint(x: 50, y: 50)
var body: some View {
GeometryReader { geometry in
ZStack {
//
Circle()
.fill(Color.gray.opacity(0.2))
.frame(width: 200, height: 200)
.contentShape(Circle())
//
Circle()
.fill(Color.white.opacity(0.7))
.frame(width: 75, height: 75)
.offset(x: CGFloat(currentDirection.x), y: CGFloat(currentDirection.y))
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let location = gesture.location
//使Geometrydxdy
let viewFrame = geometry.frame(in: .local)
let center = CGPoint(
x: viewFrame.midX,
y: viewFrame.midY
)
//let dx = location.x - centerPoint.x
//let dy = location.y - centerPoint.y
let dx = location.x - center.x
let dy = location.y - center.y
//
let radians = atan2(dy, dx)
var degrees = radians * 180 / .pi
if degrees < 0 { degrees += 360 }
//
let direction = get45DegreeDirection(from: degrees)
//
if direction.x != currentDirection.x || direction.y != currentDirection.y {
currentDirection = direction
onDirectionChanged(direction)
}
}
.onEnded { _ in
currentDirection = (0, 0)
onDirectionChanged((0, 0))
}
)
}
}
// 45
//Y
//scale 168
private func get45DegreeDirection(from degrees: CGFloat) -> (x: Int, y: Int) {
switch degrees {
// (337.5°-22.5°)
case 337.5...360, 0..<22.5:
return (50, 0)
// (22.5°-67.5°)
case 22.5..<67.5:
//return (50, -50)
return (50, 50)
// (67.5°-112.5°)
case 67.5..<112.5:
//return (0, -50)
return (0, 50)
// (112.5°-157.5°)
case 112.5..<157.5:
//return (-50, -50)
return (-50, 50)
// (157.5°-202.5°)
case 157.5..<202.5:
return (-50, 0)
// (202.5°-247.5°)
case 202.5..<247.5:
//return (-50, 50)
return (-50, -50)
// (247.5°-292.5°)
case 247.5..<292.5:
//return (0, 50)
return (0, -50)
// (292.5°-337.5°)
case 292.5..<337.5:
//return (50, 50)
return (50, -50)
default:
return (0, 0)
}
}
}
struct FreeRoamJoystick: View {
var onPositionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 90
private let thumbRadius: CGFloat = 75
@State private var isEightWay = 0 // 0: 1:
@State private var position = CGPoint.zero
@State private var viewCenter: CGPoint = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
Spacer()
//
Circle()
.stroke(Color.gray.opacity(0.5), lineWidth: 4)
.frame(width: trackRadius*2, height: trackRadius*2)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.contentShape(Circle()) // 1
//
let maxMoveRadius = trackRadius - thumbRadius
Circle()
.fill(Color.white.opacity(0.4))
.frame(width: thumbRadius*2, height: thumbRadius*2)
//.offset(x: position.x, y: position.y)
.offset(
x: min(max(position.x, -maxMoveRadius), maxMoveRadius),
y: min(max(position.y, -maxMoveRadius), maxMoveRadius)
)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
.gesture(
DragGesture(minimumDistance: 0) // 2
.onChanged { gesture in
let viewFrame = geometry.frame(in: .local)
let touchPoint = gesture.location
//
let center = CGPoint(
x: viewFrame.midX,
y: viewFrame.midY
)
let delta = CGPoint(
x: touchPoint.x - center.x,
y: touchPoint.y - center.y
)
//
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
let clampedDelta = distance > trackRadius ? CGPoint(
x: delta.x * trackRadius / distance,
y: delta.y * trackRadius / distance
) : delta
var test_data = clampedDelta
//
//
if isEightWay == 1 {
test_data = getEightDirection(for: test_data)
}
//position = clampedDelta
position = test_data
//onPositionChanged(clampedDelta)
onPositionChanged(test_data)
}
.onEnded { _ in
position = .zero
onPositionChanged(.zero)
}
)
.onAppear {
viewCenter = CGPoint(
x: geometry.size.width/2,
y: geometry.size.height/2
)
}
}
.frame(width: trackRadius, height: trackRadius)
//.frame(alignment: .bottomLeading)
}
private func getEightDirection(for delta: CGPoint) -> CGPoint {
let angle = atan2(-delta.y, delta.x)
let degrees = angle * 180 / .pi
let normalized = degrees < 0 ? degrees + 360 : degrees
switch normalized {
case 337.5...360, 0..<22.5: //
return CGPoint(x: trackRadius, y: 0)
case 22.5..<67.5: //
return CGPoint(x: trackRadius, y: -trackRadius)
case 67.5..<112.5: //
return CGPoint(x: 0, y: -trackRadius)
case 112.5..<157.5: //
return CGPoint(x: -trackRadius, y: -trackRadius)
case 157.5..<202.5: //
return CGPoint(x: -trackRadius, y: 0)
case 202.5..<247.5: //
return CGPoint(x: -trackRadius, y: trackRadius)
case 247.5..<292.5: //
return CGPoint(x: 0, y: trackRadius)
case 292.5..<337.5: //
return CGPoint(x: trackRadius, y: trackRadius)
default:
return .zero
}
}
}
struct NineGridJoystick: View {
var onDirectionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 90
private let thumbRadius: CGFloat = 35
@State private var position = CGPoint.zero
@State private var activeColor = Color.white.opacity(0.4)
//
private var gridThreshold: CGFloat {
trackRadius / 3 // 56
}
var body: some View {
GeometryReader { geometry in
ZStack {
//
gridBackgroundView()
//
Circle()
.fill(activeColor)
.frame(width: thumbRadius*2, height: thumbRadius*2)
.offset(x: position.x, y: position.y)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
//.animation(.spring(), value: position)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2)
let delta = CGPoint(
x: gesture.location.x - center.x,
y: gesture.location.y - center.y
)
let direction = getGridDirection(for: delta)
updatePosition(to: direction)
}
.onEnded { _ in
resetPosition()
}
)
}
.frame(width: trackRadius*2, height: trackRadius*2)
}
// MARK: -
private func gridBackgroundView() -> some View {
ZStack {
// 线
Path { path in
// 线
path.move(to: CGPoint(x: trackRadius - gridThreshold, y: 0))
path.addLine(to: CGPoint(x: trackRadius - gridThreshold, y: trackRadius*2))
path.move(to: CGPoint(x: trackRadius + gridThreshold, y: 0))
path.addLine(to: CGPoint(x: trackRadius + gridThreshold, y: trackRadius*2))
// 线
path.move(to: CGPoint(x: 0, y: trackRadius - gridThreshold))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius - gridThreshold))
path.move(to: CGPoint(x: 0, y: trackRadius + gridThreshold))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + gridThreshold))
}
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
//
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(width: gridThreshold*2, height: gridThreshold*2)
}
}
// MARK: -
private func getGridDirection(for delta: CGPoint) -> CGPoint {
let xVal = delta.x
let yVal = delta.y
switch (xVal, yVal) {
case (..<(-gridThreshold), ..<(-gridThreshold)): //
return CGPoint(x: -trackRadius, y: -trackRadius)
case ((-gridThreshold)...gridThreshold, ..<(-gridThreshold)): //
return CGPoint(x: 0, y: -trackRadius)
case (gridThreshold..., ..<(-gridThreshold)): //
return CGPoint(x: trackRadius, y: -trackRadius)
case (..<(-gridThreshold), (-gridThreshold)...gridThreshold): //
return CGPoint(x: -trackRadius, y: 0)
case ((-gridThreshold)...gridThreshold, (-gridThreshold)...gridThreshold): //
return .zero
case (gridThreshold..., (-gridThreshold)...gridThreshold): //
return CGPoint(x: trackRadius, y: 0)
case (..<(-gridThreshold), gridThreshold...): //
return CGPoint(x: -trackRadius, y: trackRadius)
case ((-gridThreshold)...gridThreshold, gridThreshold...): //
return CGPoint(x: 0, y: trackRadius)
case (gridThreshold..., gridThreshold...): //
return CGPoint(x: trackRadius, y: trackRadius)
default:
return .zero
}
}
// MARK: -
private func updatePosition(to direction: CGPoint) {
//withAnimation(.spring()) {
position = direction
activeColor = direction == .zero ?
Color.white.opacity(0.4) :
Color.blue.opacity(0.6)
onDirectionChanged(direction)
//}
}
private func resetPosition() {
//withAnimation(.spring()) {
position = .zero
activeColor = Color.white.opacity(0.4)
onDirectionChanged(.zero)
//}
}
}
struct DynamicNineGridJoystick: View {
var onDirectionChanged: (CGPoint) -> Void
private let trackRadius: CGFloat = 100
private let thumbRadius: CGFloat = 35
@State private var position = CGPoint.zero
@State private var activeColor = Color.white.opacity(0.4)
@State private var lastDirection: CGPoint = .zero
//
private var gridThreshold: CGFloat { trackRadius / 3 }
private let hapticGenerator = UIImpactFeedbackGenerator(style: .soft)
var body: some View {
GeometryReader { geometry in
ZStack {
//
gridBackgroundView()
//
Circle()
.fill(activeColor)
.frame(width: thumbRadius*2, height: thumbRadius*2)
.offset(x: position.x, y: position.y)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
//.animation(.spring(), value: position)
}
//
.contentShape(Rectangle()) // 使GeometryReader
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
handleGestureChange(
location: gesture.location,
center: CGPoint(x: geometry.size.width/2,
y: geometry.size.height/2)
)
}
.onEnded { _ in
resetPosition()
}
)
}
.frame(width: trackRadius*2, height: trackRadius*2)
.onAppear {
hapticGenerator.prepare()
}
}
// MARK: -
private func handleGestureChange(location: CGPoint, center: CGPoint) {
let delta = CGPoint(
x: location.x - center.x,
y: location.y - center.y
)
let currentDirection = getGridDirection(for: delta)
//
if currentDirection != lastDirection {
updatePosition(to: currentDirection)
triggerHapticFeedback()
lastDirection = currentDirection
}
}
// MARK: -
private func getGridDirection(for delta: CGPoint) -> CGPoint {
let xVal = delta.x
let yVal = delta.y
//
let isLeft = xVal < -gridThreshold
let isRight = xVal > gridThreshold
let isUp = yVal < -gridThreshold
let isDown = yVal > gridThreshold
switch (isLeft, isRight, isUp, isDown) {
case (true, false, true, false): //
return CGPoint(x: -trackRadius, y: -trackRadius)
case (false, false, true, false): //
return CGPoint(x: 0, y: -trackRadius)
case (false, true, true, false): //
return CGPoint(x: trackRadius, y: -trackRadius)
case (true, false, false, false): //
return CGPoint(x: -trackRadius, y: 0)
case (false, false, false, false): //
return .zero
case (false, true, false, false): //
return CGPoint(x: trackRadius, y: 0)
case (true, false, false, true): //
return CGPoint(x: -trackRadius, y: trackRadius)
case (false, false, false, true): //
return CGPoint(x: 0, y: trackRadius)
case (false, true, false, true): //
return CGPoint(x: trackRadius, y: trackRadius)
default: // 线
return calculateEdgeCase(delta: delta)
}
}
//
private func calculateEdgeCase(delta: CGPoint) -> CGPoint {
let angle = atan2(delta.y, delta.x) * 180 / .pi
let normalized = angle < 0 ? angle + 360 : angle
switch normalized {
case 45..<135: //
return CGPoint(x: 0, y: -trackRadius)
case 135..<225: //
return CGPoint(x: -trackRadius, y: 0)
case 225..<315: //
return CGPoint(x: 0, y: trackRadius)
default: //
return CGPoint(x: trackRadius, y: 0)
}
}
// MARK: -
private func updatePosition(to direction: CGPoint) {
//withAnimation(.interactiveSpring()) {
position = direction
activeColor = direction == .zero ?
Color.white.opacity(0.4) :
Color.blue.opacity(0.6)
onDirectionChanged(direction)
}
private func resetPosition() {
//withAnimation(.spring()) {
position = .zero
activeColor = Color.white.opacity(0.4)
lastDirection = .zero
onDirectionChanged(.zero)
//}
}
private func triggerHapticFeedback() {
guard position != .zero else { return }
hapticGenerator.impactOccurred(intensity: 0.7)
}
// MARK: -
private func gridBackgroundView() -> some View {
ZStack {
// 线
Path { path in
let offsets = [-gridThreshold, 0, gridThreshold]
for offset in offsets {
path.move(to: CGPoint(x: trackRadius + offset, y: 0))
path.addLine(to: CGPoint(x: trackRadius + offset, y: trackRadius*2))
path.move(to: CGPoint(x: 0, y: trackRadius + offset))
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + offset))
}
}
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.1))
.frame(width: gridThreshold*2, height: gridThreshold*2)
}
}
}

View File

@ -15,7 +15,7 @@ struct MetalView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView { func makeUIView(context: Context) -> UIView {
if Ryujinx.shared.emulationUIView == nil { if Ryujinx.shared.emulationUIView == nil {
var view = MeloMTKView() let view = MeloMTKView()
guard let metalLayer = view.layer as? CAMetalLayer else { guard let metalLayer = view.layer as? CAMetalLayer else {
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer") fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
@ -34,13 +34,19 @@ struct MetalView: UIViewRepresentable {
return view return view
} }
let uiview = UIView() if Double(UIDevice.current.systemVersion)! < 17.0 {
uiview.layer.addSublayer(Ryujinx.shared.metalLayer!) let uiview = MTKView()
let layer = Ryujinx.shared.metalLayer!
uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale layer.frame = uiview.bounds
uiview.layer.addSublayer(layer)
return uiview return uiview
} else {
return Ryujinx.shared.emulationUIView!
}
} }
func updateUIView(_ uiView: UIView, context: Context) { func updateUIView(_ uiView: UIView, context: Context) {

View File

@ -10,7 +10,7 @@ import MetalKit
struct TouchView: UIViewRepresentable { struct TouchView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView { func makeUIView(context: Context) -> UIView {
var view = MeloMTKView() let view = MeloMTKView()
return view return view
} }

View File

@ -258,7 +258,7 @@ struct GameLibraryView: View {
let fileExtension = (url.pathExtension as NSString).utf8String let fileExtension = (url.pathExtension as NSString).utf8String
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension) let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url) let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)

View File

@ -183,7 +183,7 @@ struct SettingsView: View {
.padding(.vertical, 8) .padding(.vertical, 8)
Toggle(isOn: $performacehud) { Toggle(isOn: $performacehud) {
labelWithIcon("Performance Overlay", iconName: "speedometer") labelWithIcon("Custom Performance Overlay", iconName: "speedometer")
} }
.tint(.blue) .tint(.blue)
} header: { } header: {
@ -452,7 +452,8 @@ struct SettingsView: View {
.tint(.blue) .tint(.blue)
.contextMenu { .contextMenu {
Button { Button {
if let mainWindow = UIApplication.shared.windows.last { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert) let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert)
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
@ -481,7 +482,8 @@ struct SettingsView: View {
}.tint(.blue) }.tint(.blue)
.contextMenu() { .contextMenu() {
Button { Button {
if let mainWindow = UIApplication.shared.windows.last { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert) let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil) let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
@ -546,7 +548,7 @@ struct SettingsView: View {
if ProcessInfo.processInfo.isiOSAppOnMac { if ProcessInfo.processInfo.isiOSAppOnMac {
labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill") labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
} else { } else {
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill") labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
} }
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo") labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
@ -577,7 +579,16 @@ struct SettingsView: View {
Spacer() Spacer()
Text("\(String(Int(getpagesize())))") Text("\(String(Int(getpagesize())))")
.foregroundColor(.secondary) .foregroundColor(.secondary)
}
if MTLHud.shared.canMetalHud {
Toggle(isOn: $metalHUDEnabled) {
labelWithIcon("Metal Performance HUD", iconName: "speedometer")
}
.tint(.blue)
.onChange(of: metalHUDEnabled) { newValue in
MTLHud.shared.toggle()
}
} }
Toggle(isOn: $ignoreJIT) { Toggle(isOn: $ignoreJIT) {
@ -617,7 +628,7 @@ struct SettingsView: View {
.textCase(nil) .textCase(nil)
.headerProminence(.increased) .headerProminence(.increased)
} footer: { } 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). \n \n\(gamepo ? "the cake is a lie" : "")") Text("For advanced users. See page size or add custom arguments for experimental features, \"Metal Performance HUD\" is not needed if you have it enabled in settings. \n \n\(gamepo ? "the cake is a lie" : "")")
} }
} }

View File

@ -2,7 +2,7 @@
// MeloNXUpdateSheet.swift // MeloNXUpdateSheet.swift
// MeloNX // MeloNX
// //
// Created by Bella on 12/03/2025. // Created by Stossy11 and Bella on 12/03/2025.
// //
import SwiftUI import SwiftUI
@ -58,7 +58,3 @@ struct MeloNXUpdateSheet: View {
} }
} }
} }
#Preview {
MeloNXUpdateSheet(updateInfo: LatestVersionResponse.example1, isPresented: .constant(true))
}

View File

@ -132,6 +132,7 @@ struct DLCManagerSheet: View {
private extension DLCManagerSheet { private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] { static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game) let jsonURL = dlcJsonPath(for: game)
try? FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard let data = try? Data(contentsOf: jsonURL), guard let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data) var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] } else { return [] }

View File

@ -179,6 +179,7 @@ struct UpdateManagerSheet: View {
do { do {
let data = try Data(contentsOf: jsonURL) let data = try Data(contentsOf: jsonURL)
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection { if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {

View File

@ -8,39 +8,29 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
import CryptoKit import CryptoKit
import Alamofire
@main @main
struct MeloNXApp: App { struct MeloNXApp: App {
@Environment(\.scenePhase) var scenePhase
@State var finished = false
// Variables for the update system :) @State var showed = false
@Environment(\.scenePhase) var scenePhase
@State var alert: UIAlertController? = nil
@State var showOutOfDateSheet = false @State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil @State var updateInfo: LatestVersionResponse? = nil
@State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
VStack {
if finishedStorage { if finishedStorage {
ContentView() ContentView()
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
withAnimation {
finishedStorage = newValue
}
}
}
}
}
.onAppear { .onAppear {
checkLatestVersion() checkLatestVersion()
} }
// this seems like a weird way to show the sheet but, from my history this is the most reliable way for the content to actually show in the sheet, otherwise its blank
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil }, get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in set: { newValue in
@ -54,55 +44,60 @@ struct MeloNXApp: App {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet) MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
} }
} }
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
withAnimation {
finishedStorage = newValue
}
}
}
}
} }
} }
// sends a GET request to the MeloNXSite API and compares the version it returns to the current app version
func checkLatestVersion() { func checkLatestVersion() {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "") let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
#if DEBUG #if DEBUG
// no this isnt a public ip address silly viewers (i know damn well someone thought this was my real ip), this is local :PP let urlString = "http://192.168.178.116:8000/api/latest_release"
let url = "http://192.168.178.116:8000/api/latest_release"
#else #else
// dont spam this :pray: let urlString = "https://melonx.org/api/latest_release"
let url = "https://melonx.org/api/latest_release"
#endif #endif
// actually sends the request guard let url = URL(string: urlString) else {
AF.request(url).responseDecodable(of: LatestVersionResponse.self) { response in print("Invalid URL")
switch response.result { return
case .success(let latestVersionResponse): }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error checking for new version: \(error)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data)
let latestAPIVersionStripped = latestVersionResponse.version_number_stripped let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
if Int(strippedAppVersion) ?? 0 < Int(latestAPIVersionStripped) ?? 0 {
if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 {
DispatchQueue.main.async {
updateInfo = latestVersionResponse updateInfo = latestVersionResponse
showOutOfDateSheet = true showOutOfDateSheet = true
} }
case .failure(let error):
print("Error checking for new version: \(error)")
}
} }
} catch {
print("Failed to decode response: \(error)")
} }
} }
func detectRoms(path string: String) -> String { task.resume()
let inputData = Data(string.utf8)
let romHash = SHA256.hash(data: inputData)
return romHash.compactMap { String(format: "%02x", $0) }.joined()
}
func addFolders(_ folderPath: String) -> String? {
let fileManager = FileManager.default
if let data = Data(base64Encoded: folderPath),
let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString {
return decodedString + "auth/" + fileURL + "/"
}
return nil
}
extension String {
func print() {
Swift.print(self)
} }
} }

View File

@ -65,6 +65,7 @@ struct SetupView: View {
initialize() initialize()
finished = false finished = false
keysImported = Ryujinx.shared.checkIfKeysImported() keysImported = Ryujinx.shared.checkIfKeysImported()
print((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0))
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
} }
} }
@ -116,6 +117,9 @@ struct SetupView: View {
.font(.title) .font(.title)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(.primary) .foregroundColor(.primary)
.onTapGesture(count: 2) {
showSkipAlert = true
}
Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.") Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
.font(.subheadline) .font(.subheadline)
@ -365,6 +369,7 @@ struct SetupView: View {
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path) Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0)
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
alertMessage = "Firmware installed successfully" alertMessage = "Firmware installed successfully"

View File

@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
TamperMachine, TamperMachine,
UI, UI,
Vic, Vic,
Memory,
} }
} }

View File

@ -4,6 +4,7 @@ using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
namespace Ryujinx.Cpu.LightningJit.Cache namespace Ryujinx.Cpu.LightningJit.Cache
{ {
@ -21,12 +22,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
private readonly ReservedRegion _region; private readonly ReservedRegion _region;
private readonly CacheMemoryAllocator _cacheAllocator; private readonly CacheMemoryAllocator _cacheAllocator;
public readonly IJitMemoryAllocator Allocator;
public CacheMemoryAllocator Allocator => _cacheAllocator; public CacheMemoryAllocator CacheAllocator => _cacheAllocator;
public IntPtr Pointer => _region.Block.Pointer; public IntPtr Pointer => _region.Block.Pointer;
public MemoryCache(IJitMemoryAllocator allocator, ulong size) public MemoryCache(IJitMemoryAllocator allocator, ulong size)
{ {
Allocator = allocator;
_region = new(allocator, size); _region = new(allocator, size);
_cacheAllocator = new((int)size); _cacheAllocator = new((int)size);
} }
@ -101,9 +104,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private readonly IStackWalker _stackWalker; private readonly IStackWalker _stackWalker;
private readonly Translator _translator; private readonly Translator _translator;
private readonly MemoryCache _sharedCache; private readonly List<MemoryCache> _sharedCaches;
private readonly MemoryCache _localCache; private readonly List<MemoryCache> _localCaches;
private readonly PageAlignedRangeList _pendingMap; private readonly Dictionary<ulong, PageAlignedRangeList> _pendingMaps;
private readonly object _lock; private readonly object _lock;
class ThreadLocalCacheEntry class ThreadLocalCacheEntry
@ -111,13 +114,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
public readonly int Offset; public readonly int Offset;
public readonly int Size; public readonly int Size;
public readonly IntPtr FuncPtr; public readonly IntPtr FuncPtr;
public readonly int CacheIndex;
private int _useCount; private int _useCount;
public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr) public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr, int cacheIndex)
{ {
Offset = offset; Offset = offset;
Size = size; Size = size;
FuncPtr = funcPtr; FuncPtr = funcPtr;
CacheIndex = cacheIndex;
_useCount = 0; _useCount = 0;
} }
@ -134,12 +139,87 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
_stackWalker = stackWalker; _stackWalker = stackWalker;
_translator = translator; _translator = translator;
_sharedCache = new(allocator, SharedCacheSize); _sharedCaches = new List<MemoryCache> { new(allocator, SharedCacheSize) };
_localCache = new(allocator, LocalCacheSize); _localCaches = new List<MemoryCache> { new(allocator, LocalCacheSize) };
_pendingMap = new(_sharedCache.ReprotectAsRx, RegisterFunction); _pendingMaps = new Dictionary<ulong, PageAlignedRangeList>();
_lock = new(); _lock = new();
} }
private PageAlignedRangeList GetPendingMapForCache(int cacheIndex)
{
ulong cacheKey = (ulong)cacheIndex;
if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap))
{
pendingMap = new PageAlignedRangeList(
(offset, size) => _sharedCaches[cacheIndex].ReprotectAsRx(offset, size),
(address, func) => RegisterFunction(address, func));
_pendingMaps[cacheKey] = pendingMap;
}
return pendingMap;
}
private bool HasInAnyPendingMap(ulong guestAddress)
{
foreach (var pendingMap in _pendingMaps.Values)
{
if (pendingMap.Has(guestAddress))
{
return true;
}
}
return false;
}
private int AllocateInSharedCache(int codeLength)
{
for (int i = 0; i < _sharedCaches.Count; i++)
{
try
{
return (i << 28) | _sharedCaches[i].Allocate(codeLength);
}
catch (OutOfMemoryException)
{
// Try next cache
}
}
// All existing caches are full, create a new one
lock (_lock)
{
var allocator = _sharedCaches[0].Allocator;
_sharedCaches.Add(new(allocator, SharedCacheSize));
return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength);
}
}
private int AllocateInLocalCache(int codeLength)
{
for (int i = 0; i < _localCaches.Count; i++)
{
try
{
return (i << 28) | _localCaches[i].Allocate(codeLength);
}
catch (OutOfMemoryException)
{
}
}
lock (_lock)
{
var allocator = _localCaches[0].Allocator;
_localCaches.Add(new(allocator, LocalCacheSize));
return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength);
}
}
private static (int cacheIndex, int offset) SplitCacheOffset(int combinedOffset)
{
return (combinedOffset >> 28, combinedOffset & 0xFFFFFFF);
}
public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan<byte> code, ulong guestAddress, ulong guestSize) public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan<byte> code, ulong guestAddress, ulong guestSize)
{ {
if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr)) if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr))
@ -149,16 +229,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
lock (_lock) lock (_lock)
{ {
if (!_pendingMap.Has(guestAddress) && !_translator.Functions.ContainsKey(guestAddress)) if (!HasInAnyPendingMap(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
{ {
int funcOffset = _sharedCache.Allocate(code.Length); int combinedOffset = AllocateInSharedCache(code.Length);
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
funcPtr = _sharedCache.Pointer + funcOffset; MemoryCache cache = _sharedCaches[cacheIndex];
funcPtr = cache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
TranslatedFunction function = new(funcPtr, guestSize); TranslatedFunction function = new(funcPtr, guestSize);
_pendingMap.Add(funcOffset, code.Length, guestAddress, function); GetPendingMapForCache(cacheIndex).Add(funcOffset, code.Length, guestAddress, function);
} }
ClearThreadLocalCache(framePointer); ClearThreadLocalCache(framePointer);
@ -171,18 +253,56 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
lock (_lock) lock (_lock)
{ {
// Ensure we will get an aligned offset from the allocator. int cacheIndex;
_pendingMap.Pad(_sharedCache.Allocator); int funcOffset;
IntPtr mappedFuncPtr = IntPtr.Zero;
for (cacheIndex = 0; cacheIndex < _sharedCaches.Count; cacheIndex++)
{
try
{
var pendingMap = GetPendingMapForCache(cacheIndex);
pendingMap.Pad(_sharedCaches[cacheIndex].CacheAllocator);
int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
int funcOffset = _sharedCache.Allocate(sizeAligned); funcOffset = _sharedCaches[cacheIndex].Allocate(sizeAligned);
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0); Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
IntPtr funcPtr = _sharedCache.Pointer + funcOffset; IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr1, code.Length));
_sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned);
return funcPtr1;
}
catch (OutOfMemoryException)
{
// Try next cache
}
}
// All existing caches are full, create a new one
var allocator = _sharedCaches[0].Allocator;
var newCache = new MemoryCache(allocator, SharedCacheSize);
_sharedCaches.Add(newCache);
cacheIndex = _sharedCaches.Count - 1;
var newPendingMap = GetPendingMapForCache(cacheIndex);
// Ensure we will get an aligned offset from the allocator.
newPendingMap.Pad(newCache.CacheAllocator);
int newSizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
funcOffset = newCache.Allocate(newSizeAligned);
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
IntPtr funcPtr = newCache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
_sharedCache.ReprotectAsRx(funcOffset, sizeAligned); newCache.ReprotectAsRx(funcOffset, newSizeAligned);
return funcPtr; return funcPtr;
} }
@ -200,7 +320,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
lock (_lock) lock (_lock)
{ {
_pendingMap.Pad(_sharedCache.Allocator); foreach (var pendingMap in _pendingMaps.Values)
{
// Get the cache index from the pendingMap key
if (_pendingMaps.FirstOrDefault(x => x.Value == pendingMap).Key is ulong cacheIndex)
{
// Use the correct shared cache for padding based on the cache index
pendingMap.Pad(_sharedCaches[(int)cacheIndex].CacheAllocator);
}
}
} }
} }
@ -224,12 +352,36 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return; return;
} }
IEnumerable<ulong> callStack = _stackWalker.GetCallStack( IntPtr[] cachePointers = new IntPtr[_localCaches.Count];
int[] cacheSizes = new int[_localCaches.Count];
for (int i = 0; i < _localCaches.Count; i++)
{
cachePointers[i] = _localCaches[i].Pointer;
cacheSizes[i] = LocalCacheSize;
}
IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count];
int[] sharedSizes = new int[_sharedCaches.Count];
for (int i = 0; i < _sharedCaches.Count; i++)
{
sharedPointers[i] = _sharedCaches[i].Pointer;
sharedSizes[i] = SharedCacheSize;
}
// Iterate over the arrays and pass each element to GetCallStack
IEnumerable<ulong> callStack = null;
for (int i = 0; i < _localCaches.Count; i++)
{
callStack = _stackWalker.GetCallStack(
framePointer, framePointer,
_localCache.Pointer, cachePointers[i], // Passing each individual cachePointer
LocalCacheSize, cacheSizes[i], // Passing each individual cacheSize
_sharedCache.Pointer, sharedPointers[i], // Passing each individual sharedPointer
SharedCacheSize); sharedSizes[i] // Passing each individual sharedSize
);
}
List<(ulong, ThreadLocalCacheEntry)> toDelete = new(); List<(ulong, ThreadLocalCacheEntry)> toDelete = new();
@ -237,7 +389,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
// We only want to delete if the function is already on the shared cache, // We only want to delete if the function is already on the shared cache,
// otherwise we will keep translating the same function over and over again. // otherwise we will keep translating the same function over and over again.
bool canDelete = !_pendingMap.Has(address); bool canDelete = !HasInAnyPendingMap(address);
if (!canDelete) if (!canDelete)
{ {
continue; continue;
@ -267,12 +419,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
_threadLocalCache.Remove(address); _threadLocalCache.Remove(address);
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize); int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCache.Free(entry.Offset, sizeAligned); _localCaches[cacheIndex].Free(offset, sizeAligned);
_localCache.ReprotectAsRw(entry.Offset, sizeAligned); _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
} }
} }
public void ClearEntireThreadLocalCache() public void ClearEntireThreadLocalCache()
{ {
// Thread is exiting, delete everything. // Thread is exiting, delete everything.
@ -287,9 +441,10 @@ namespace Ryujinx.Cpu.LightningJit.Cache
foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache) foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache)
{ {
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize); int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCache.Free(entry.Offset, sizeAligned); _localCaches[cacheIndex].Free(offset, sizeAligned);
_localCache.ReprotectAsRw(entry.Offset, sizeAligned); _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
} }
_threadLocalCache.Clear(); _threadLocalCache.Clear();
@ -299,16 +454,17 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan<byte> code, ulong guestAddress) private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan<byte> code, ulong guestAddress)
{ {
int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize()); int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
int funcOffset = _localCache.Allocate(alignedSize); int combinedOffset = AllocateInLocalCache(alignedSize);
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0); Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
IntPtr funcPtr = _localCache.Pointer + funcOffset; IntPtr funcPtr = _localCaches[cacheIndex].Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length)); code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr)); (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex));
_localCache.ReprotectAsRx(funcOffset, alignedSize); _localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize);
return funcPtr; return funcPtr;
} }
@ -326,8 +482,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{ {
if (disposing) if (disposing)
{ {
_localCache.Dispose(); foreach (var cache in _localCaches)
_sharedCache.Dispose(); {
cache.Dispose();
}
foreach (var cache in _sharedCaches)
{
cache.Dispose();
}
_localCaches.Clear();
_sharedCaches.Clear();
} }
} }

View File

@ -769,16 +769,77 @@ namespace Ryujinx.Graphics.Vulkan
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null) private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
{ {
const int MaxChunkSize = 1024 * 1024 * 16; // 16MB chunks
int bufferDataLength = GetBufferDataLength(data.Length); int bufferDataLength = GetBufferDataLength(data.Length);
using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength); if (bufferDataLength <= MaxChunkSize)
{
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
return;
}
Auto<DisposableImage> imageAuto = GetImage(); if (!region.HasValue && !singleSlice && layers > 1)
{
int layerSize = data.Length / layers;
int offset = 0;
// Load texture data inline if the texture has been used on the current command buffer. for (int i = 0; i < layers; i++)
{
int currentLayer = layer + i;
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
var layerData = data.Slice(offset, currentLayerSize);
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
offset += layerSize;
if (offset >= data.Length)
break;
}
}
else if (region.HasValue)
{
var rect = region.Value;
int dataPerPixel = data.Length / (rect.Width * rect.Height);
int rowStride = rect.Width * dataPerPixel;
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
int originalHeight = rect.Height;
int currentY = rect.Y;
int offset = 0;
while (currentY < rect.Y + originalHeight)
{
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
int chunkSize = chunkHeight * rowStride;
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
var chunkData = data.Slice(offset, safeChunkSize);
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
currentY += chunkHeight;
offset += chunkSize;
if (offset >= data.Length)
break;
}
}
else
{
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
}
void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
{
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
using (var imageAuto = GetImage())
{
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer); bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer(); var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
if (loadInline) if (loadInline)
@ -786,29 +847,41 @@ namespace Ryujinx.Graphics.Vulkan
_gd.PipelineInternal.EndRenderPass(); _gd.PipelineInternal.EndRenderPass();
} }
CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data); CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = imageAuto.Get(cbs).Value; var image = imageAuto.Get(cbs).Value;
if (region.HasValue) if (chunkRegion.HasValue)
{ {
CopyFromOrToBuffer( CopyFromOrToBuffer(
cbs.CommandBuffer, cbs.CommandBuffer,
buffer, buffer,
image, image,
bufferDataLength, chunkBufferLength,
false, false,
layer, chunkLayer,
level, chunkLevel,
region.Value.X, chunkRegion.Value.X,
region.Value.Y, chunkRegion.Value.Y,
region.Value.Width, chunkRegion.Value.Width,
region.Value.Height); chunkRegion.Value.Height);
} }
else else
{ {
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice); CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
chunkBufferLength,
false,
chunkLayer,
chunkLevel,
chunkLayers,
chunkLevels,
chunkSingleSlice);
}
}
} }
} }

View File

@ -44,7 +44,7 @@ namespace Ryujinx.HLE
MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable
? MemoryAllocationFlags.Reserve ? MemoryAllocationFlags.Reserve
: MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable; : MemoryAllocationFlags.Reserve; // | MemoryAllocationFlags.Mirrorable;
#pragma warning disable IDE0055 // Disable formatting #pragma warning disable IDE0055 // Disable formatting
AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver); AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver);