Compare commits

..

17 Commits

Author SHA1 Message Date
667d54ed2d okay NOW we're in business🤑 2025-03-20 15:11:56 +00:00
1b70bfea8b its not resized properly. shit. brb. 2025-03-20 15:07:43 +00:00
33b8571414 Replace app icon with the new one 🤑 2025-03-20 15:06:16 +00:00
33af004d85 Delete src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png 2025-03-20 15:02:30 +00:00
Stossy11
54cb7eb953 Updated JitStreamer Implementation, Reimplemented Texture Chunks, Reworked Alerts and more 2025-03-20 21:33:28 +11:00
ceab2f0ac8 Fix spelling mistake in README 2025-03-16 10:05:09 +00:00
7986859398 Update README.md 2025-03-16 10:02:34 +00:00
c4506da8a1 Update README.md 2025-03-15 21:10:43 +00:00
9f72c9da10 Update README.md 2025-03-15 21:03:36 +00:00
ba0c49f545 Update README With Experimental Free Developer Account guide 2025-03-15 21:02:49 +00:00
80148ac69a :sus64: 2025-03-13 06:42:26 +00:00
Stossy11
7417ddfeef Update version number 2025-03-13 17:18:42 +11:00
Stossy11
f66590203a Fix Setup 2025-03-13 09:40:18 +11:00
stossy11
f091e6c5ea Update README.md 2025-03-12 22:36:03 +00:00
stossy11
75a66586b2 Update README.md 2025-03-12 22:32:21 +00:00
Stossy11
3207e1e739 Merge pull request 'Fix for updates and DLCs' (#21) from XITRIX/MeloNX:updates-fix into XC-ios-ht 2025-03-13 09:30:28 +11:00
Stossy11
a6b4f2d91f Rewrite the menu Code, Add Metal HUD to advanced options and more 2025-03-13 09:05:21 +11:00
52 changed files with 1955 additions and 1245 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>
@ -28,11 +23,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
## 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 +48,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 +117,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 +146,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

@ -41,4 +41,6 @@ ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
# Update the xcconfig file # Update the xcconfig file
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE" sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
$DOTNET_PATH clean
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH" echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"

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.7.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 */;
@ -117,7 +116,7 @@
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = ( "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
CodeSignOnCopy, CodeSignOnCopy,
); );
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = ( "Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
); );
@ -178,7 +177,7 @@
"Dependencies/Dynamic Libraries/libavutil.dylib", "Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib", "Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib", "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework", "Dependencies/Dynamic Libraries/RyujinxHelper.framework",
Dependencies/XCFrameworks/libavcodec.xcframework, Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework, Dependencies/XCFrameworks/libavformat.xcframework,
@ -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,12 @@
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
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 +498,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 +652,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 = (
@ -716,8 +713,16 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
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,13 +845,30 @@
"$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
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";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -864,7 +886,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 = (
@ -925,8 +947,16 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
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,13 +1079,30 @@
"$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = "$(VERSION)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
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";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -1251,14 +1298,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 +1317,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,8 +64,10 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
showGraphicsOverview = "Yes" allowLocationSimulation = "YES"
allowLocationSimulation = "YES"> viewDebuggingEnabled = "No"
consoleMode = "0"
structuredConsoleMode = "2">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
@ -105,8 +107,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

@ -6,39 +6,121 @@
// //
import Foundation import Foundation
import Network
import UIKit
func enableJITEB() { func enableJITEB() {
guard let bundleID = Bundle.main.bundleIdentifier else { if UserDefaults.standard.bool(forKey: "waitForVPN") {
waitForVPNConnection { connected in
if connected {
enableJITEBRequest()
}
}
} else {
enableJITEBRequest()
}
}
func enableJITEBRequest() {
let pid = Int(getpid())
print(pid)
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
presentAlert(title: "Request Error", message: error.localizedDescription)
return return
} }
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
let task = URLSession.shared.dataTask(with: address) { data, response, error in
if error != nil {
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 data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
} else {
fatalError("Unable to get Window")
}
} }
return
} }
task.resume() task.resume()
} }
func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
let startTime = Date()
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
timer.schedule(deadline: .now(), repeating: interval)
timer.setEventHandler {
pingSite { connected in
if connected {
timer.cancel()
DispatchQueue.main.async {
completion(true)
}
} else if Date().timeIntervalSince(startTime) > timeout {
timer.cancel()
DispatchQueue.main.async {
completion(false)
}
}
}
}
timer.resume()
}
func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
guard let url = URL(string: host) else {
completion(false)
return
}
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 2.0
config.timeoutIntervalForResource = 2.0
let session = URLSession(configuration: config)
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { _, response, error in
if let error = error {
print("Ping failed: \(error.localizedDescription)")
completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true)
} else {
let httpResponse = response as? HTTPURLResponse
completion(false)
}
}
task.resume()
}
func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let lastWindow = windowScene.windows.last {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?()
})
DispatchQueue.main.async {
lastWindow.rootViewController?.present(alert, animated: true)
}
}
}
struct LaunchApp: Codable { struct LaunchApp: Codable {
let ok: Bool let success: Bool
let error: String? let message: String
let launching: Bool
let position: Int?
let mounting: Bool
} }
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) { func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
@ -47,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
var message = "" var message = ""
if let error = result.error { if !result.success {
message = "Error: \(error)" message += "\n\(result.message)"
} else if result.mounting {
message = "App is mounting..."
} else if result.launching {
message = "App is launching..."
} else {
message = "App launch status unknown."
}
if let position = result.position {
message += "\nPosition: \(position)"
}
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert) let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default)) alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async { DispatchQueue.main.async {
viewController.present(alert, animated: true) viewController.present(alert, animated: true)
} }
} else {
print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled()
}
} catch { } catch {
print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert) let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default)) alert.addAction(UIAlertAction(title: "OK", style: .default))

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 {
@ -191,6 +189,24 @@ enum VirtualControllerButton: Int {
case dPadRight case dPadRight
case leftTrigger case leftTrigger
case rightTrigger case rightTrigger
var isTrigger: Bool {
switch self {
case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
return true
default:
return false
}
}
var isSmall: Bool {
switch self {
case .back, .start, .guide:
return true
default:
return false
}
}
} }
enum ThumbstickType: Int { enum ThumbstickType: Int {

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

@ -31,7 +31,7 @@ struct iOSNav<Content: View>: View {
} }
class Ryujinx { class Ryujinx : ObservableObject {
private var isRunning = false private var isRunning = false
let virtualController = VirtualController() let virtualController = VirtualController()
@ -45,6 +45,10 @@ class Ryujinx {
@Published var defMLContentSize: CGFloat? @Published var defMLContentSize: CGFloat?
var thread: Thread!
@Published var jitenabled = false
var shouldMetal: Bool { var shouldMetal: Bool {
metalLayer == nil metalLayer == nil
} }
@ -145,7 +149,7 @@ class Ryujinx {
self.config = config self.config = config
RunLoop.current.perform { [self] in thread = Thread { [self] in
isRunning = true isRunning = true
@ -178,6 +182,10 @@ class Ryujinx {
Self.log("Emulation failed to start: \(error)") Self.log("Emulation failed to start: \(error)")
} }
} }
thread.qualityOfService = .background
thread.name = "MeloNX"
thread.start()
} }
@ -192,6 +200,7 @@ class Ryujinx {
self.metalLayer = nil self.metalLayer = nil
stop_emulation() stop_emulation()
thread.cancel()
} }
var running: Bool { var running: Bool {
@ -360,7 +369,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 +378,6 @@ class Ryujinx {
return firmwareVersion return firmwareVersion
} }
} catch {
print(error)
}
return "0" return "0"
} }
@ -501,66 +505,15 @@ 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)")
} }
func ryuIsJITEnabled() {
jitenabled = isJITEnabled()
print("JIT \(jitenabled)")
}
} }

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?
@ -53,145 +53,155 @@ struct ContentView: View {
private let animationDuration: Double = 1.0 private let animationDuration: Double = 1.0
@State private var isAnimating = false @State private var isAnimating = false
@State var isLoading = true @State var isLoading = true
@State var jitNotEnabled = false @StateObject var ryujinx = Ryujinx.shared
// 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"),
] ]
_settings = State(initialValue: defaultSettings) _settings = State(initialValue: defaultSettings)
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL() initializeSDL()
} }
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
if game != nil, !jitNotEnabled { if game != nil && (ryujinx.jitenabled || ignoreJIT) {
// This is when the game starts to stop the animation gameView
ZStack { } else if game != nil && !ryujinx.jitenabled {
if #available(iOS 16, *) { jitErrorView
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else { } else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black
.opacity(0.8)
emulationView
.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
} else if game != nil, ignoreJIT {
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black
.opacity(0.8)
emulationView
.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
} else if game != nil {
Text("")
.sheet(isPresented: $jitNotEnabled) {
JITPopover() {
jitNotEnabled = false
}
.interactiveDismissDisabled()
}
} else {
// This is the main menu view that includes the Settings and the Game Selector
mainMenuView mainMenuView
.onAppear() {
quits = false
loadSettings()
isLoading = true
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
}
.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()
}
}
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()
}
} }
} }
// MARK: - View Components // MARK: - View Components
private var gameView: some View {
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black.opacity(0.8)
emulationView.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
}
private var jitErrorView: some View {
Text("")
.sheet(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
ryujinx.ryuIsJITEnabled()
})
) {
JITPopover() {
ryujinx.jitenabled = false
}
.interactiveDismissDisabled()
}
}
private var mainMenuView: some View {
MainTabView(
startemu: $game,
config: $config,
MVKconfig: $settings,
controllersList: $controllersList,
currentControllers: $currentControllers,
onscreencontroller: $onscreencontroller
)
.onAppear {
quits = false
let _ = loadSettings()
isLoading = true
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
refreshControllersList()
}
print(MTLHud.shared.isEnabled)
initControllerObservers()
Air.play(AnyView(
VStack {
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
))
checkJitStatus()
}
.onOpenURL { url in
handleDeepLink(url)
}
}
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 +219,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 +256,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 +272,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)
@ -318,9 +317,9 @@ struct ContentView: View {
} }
private func refreshControllersList() { private func refreshControllersList() {
controllersList = Ryujinx.shared.getConnectedControllers() controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) { if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
self.onscreencontroller = onscreen self.onscreencontroller = onscreen
} }
@ -330,8 +329,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,44 +339,65 @@ 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.start(with: config)
} catch { } catch {
print("Error: \(error.localizedDescription)") print("Error: \(error.localizedDescription)")
} }
} }
private func configureEnvironmentVariables() {
if mVKPreFillBuffer {
mVKPreFillBuffer = false
// 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() {
ryujinx.ryuIsJITEnabled()
if !ryujinx.jitenabled {
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.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = ryujinx.games.first(where: { $0.titleName == text })
}
}
}
} }
extension Array { extension Array {

View File

@ -11,101 +11,155 @@ import SwiftUIJoystick
import CoreMotion import CoreMotion
struct ControllerView: View { struct ControllerView: View {
// MARK: - Properties
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
@AppStorage("stick-button") private var stickButton = false
@State private var isPortrait = true
@Environment(\.verticalSizeClass) var verticalSizeClass
// MARK: - Body
var body: some View { var body: some View {
GeometryReader { geometry in Group {
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad { let isPad = UIDevice.current.userInterfaceIdiom == .pad
VStack {
Spacer()
VStack {
HStack {
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this works
ABXYView()
}
}
}
HStack {
ButtonView(button: .start) // Adding the + button
.padding(.horizontal, 40)
ButtonView(button: .back) // Adding the - button
.padding(.horizontal, 40)
}
}
}
if isPortrait && !isPad {
portraitLayout
} else { } else {
// could be landscape landscapeLayout
VStack {
Spacer()
VStack {
HStack {
// gotta fuckin add + and - now
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
HStack {
// Spacer()
VStack {
// Spacer()
ButtonView(button: .back) // Adding the - button
}
Spacer()
VStack {
// Spacer()
ButtonView(button: .start) // Adding the + button
}
// Spacer()
}
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this work s
ABXYView()
}
}
}
}
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
}
} }
} }
.padding() .padding()
.onChange(of: verticalSizeClass) { _ in
updateOrientation()
}
.onAppear(perform: updateOrientation)
}
// MARK: - Layouts
private var portraitLayout: some View {
VStack {
Spacer()
VStack(spacing: 20) {
HStack(spacing: 30) {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
} }
} }
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
HStack(spacing: 60) {
HStack {
ButtonView(button: .leftStick)
.padding()
ButtonView(button: .start)
}
HStack {
ButtonView(button: .back)
ButtonView(button: .rightStick)
.padding()
}
}
}
}
}
private var landscapeLayout: some View {
VStack {
Spacer()
HStack {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
}
}
private var centerButtons: some View {
Group {
if stickButton {
VStack {
HStack(spacing: 50) {
ButtonView(button: .leftStick)
.padding()
Spacer()
ButtonView(button: .rightStick)
.padding()
}
.padding(.top, 30)
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
}
.padding(.bottom, 20)
} else {
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
.padding(.bottom, 20)
}
}
}
// MARK: - Methods
private func updateOrientation() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
isPortrait = window.bounds.size.height > window.bounds.size.width
}
}
}
struct ShoulderButtonsViewLeft: View { struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160 @State private var width: CGFloat = 160
@State var height: CGFloat = 20 @State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack(spacing: 20) {
ButtonView(button: .leftTrigger) ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder) ButtonView(button: .leftShoulder)
.padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
@ -118,19 +172,17 @@ struct ShoulderButtonsViewLeft: View {
} }
struct ShoulderButtonsViewRight: View { struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160 @State private var width: CGFloat = 160
@State var height: CGFloat = 20 @State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack(spacing: 20) {
ButtonView(button: .rightShoulder) ButtonView(button: .rightShoulder)
.padding(.horizontal)
ButtonView(button: .rightTrigger) ButtonView(button: .rightTrigger)
.padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
@ -143,21 +195,21 @@ struct ShoulderButtonsViewRight: View {
} }
struct DPadView: View { struct DPadView: View {
@State var size: CGFloat = 145 @State private 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(spacing: 5) {
ButtonView(button: .dPadUp) ButtonView(button: .dPadUp)
HStack { HStack(spacing: 20) {
ButtonView(button: .dPadLeft) ButtonView(button: .dPadLeft)
Spacer(minLength: 20) Spacer(minLength: 20)
ButtonView(button: .dPadRight) ButtonView(button: .dPadRight)
} }
ButtonView(button: .dPadDown) ButtonView(button: .dPadDown)
.padding(.horizontal)
} }
.frame(width: size, height: size) .frame(width: size, height: size)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
@ -168,22 +220,21 @@ struct DPadView: View {
} }
struct ABXYView: View { struct ABXYView: View {
@State var size: CGFloat = 145 @State private 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(spacing: 5) {
ButtonView(button: .X) ButtonView(button: .X)
HStack { HStack(spacing: 20) {
ButtonView(button: .Y) ButtonView(button: .Y)
Spacer(minLength: 20) Spacer(minLength: 20)
ButtonView(button: .A) ButtonView(button: .A)
} }
ButtonView(button: .B) ButtonView(button: .B)
.padding(.horizontal)
} }
.frame(width: size, height: size) .frame(width: size, height: size)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
@ -195,47 +246,82 @@ struct ABXYView: View {
struct ButtonView: View { struct ButtonView: View {
var button: VirtualControllerButton var button: VirtualControllerButton
@State var width: CGFloat = 45 @State private var width: CGFloat = 45
@State var height: CGFloat = 45 @State private var height: CGFloat = 45
@State var isPressed = false @State private var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var debounceTimer: Timer?
var body: some View { var body: some View {
Image(systemName: buttonText) Image(systemName: buttonText)
.resizable() .resizable()
.scaledToFit()
.frame(width: width, height: height) .frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) .foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
.opacity(isPressed ? 0.4 : 0.7) .background(
Group {
if !button.isTrigger {
Circle()
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
.frame(width: width * 1.25, height: height * 1.25)
} else {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width * 1.25, height: height * 1.25)
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
}
}
)
.opacity(isPressed ? 0.6 : 1.0)
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in
if !self.isPressed { handleButtonPress()
self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy)
}
} }
.onEnded { _ in .onEnded { _ in
self.isPressed = false handleButtonRelease()
Ryujinx.shared.virtualController.setButtonState(0, for: button)
} }
) )
.onAppear() { .onAppear {
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder { configureSizeForButton()
width = 65 }
} }
private func handleButtonPress() {
if !isPressed {
isPressed = true
if button == .back || button == .start || button == .guide { debounceTimer?.invalidate()
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium)
}
}
private func handleButtonRelease() {
if isPressed {
isPressed = false
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
}
}
private func configureSizeForButton() {
if button.isTrigger {
width = 70
height = 40
} else if button.isSmall {
width = 35 width = 35
height = 35 height = 35
} }
// Adjust for iPad
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
@ -244,9 +330,6 @@ struct ButtonView: View {
width *= CGFloat(controllerScale) width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale) height *= CGFloat(controllerScale)
} }
}
private var buttonText: String { private var buttonText: String {
switch button { switch button {
@ -258,6 +341,10 @@ struct ButtonView: View {
return "x.circle.fill" return "x.circle.fill"
case .Y: case .Y:
return "y.circle.fill" return "y.circle.fill"
case .leftStick:
return "l.joystick.press.down.fill"
case .rightStick:
return "r.joystick.press.down.fill"
case .dPadUp: case .dPadUp:
return "arrowtriangle.up.circle.fill" return "arrowtriangle.up.circle.fill"
case .dPadDown: case .dPadDown:
@ -275,16 +362,11 @@ struct ButtonView: View {
case .rightShoulder: case .rightShoulder:
return "r.rectangle.roundedbottom.fill" return "r.rectangle.roundedbottom.fill"
case .start: case .start:
return "plus.circle.fill" // System symbol for + return "plus.circle.fill"
case .back: case .back:
return "minus.circle.fill" // System symbol for - return "minus.circle.fill"
case .guide: case .guide:
return "house.circle.fill" return "house.circle.fill"
// This should be all the cases
default:
return ""
} }
} }
} }

View File

@ -11,7 +11,7 @@ import SwiftUIJoystick
public struct Joystick: View { public struct Joystick: View {
@State var iscool: Bool? = nil @State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@ObservedObject public var joystickMonitor = JoystickMonitor() @ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat { var dragDiameter: CGFloat {
@ -36,8 +36,13 @@ public struct Joystick: View {
.hidden() .hidden()
}, },
foreground: { foreground: {
Circle().fill(Color.gray) Circle()
.opacity(0.7) .fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
.background(
Circle()
.fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2))
.frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2)
)
}, },
locksInPlace: false) locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in .onChange(of: self.joystickMonitor.xyPoint) { newValue in

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

@ -29,6 +29,7 @@ struct GameLibraryView: View {
@State var isViewingGameInfo: Bool = false @State var isViewingGameInfo: Bool = false
@State var isSelectingGameUpdate: Bool = false @State var isSelectingGameUpdate: Bool = false
@State var isSelectingGameDLC: Bool = false @State var isSelectingGameDLC: Bool = false
@StateObject var ryujinx = Ryujinx.shared
@State var gameInfo: Game? @State var gameInfo: Game?
var games: Binding<[Game]> { var games: Binding<[Game]> {
Binding( Binding(
@ -203,6 +204,13 @@ struct GameLibraryView: View {
.foregroundColor(.blue) .foregroundColor(.blue)
} }
} }
ToolbarItem(placement: .topBarLeading) {
if ryujinx.jitenabled {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
}
} }
.onChange(of: startemu) { game in .onChange(of: startemu) { game in
guard let game else { return } guard let game else { return }
@ -258,7 +266,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

@ -37,6 +37,8 @@ struct JITPopover: View {
if isJIT { if isJIT {
dismiss() dismiss()
onJITEnabled() onJITEnabled()
Ryujinx.shared.ryuIsJITEnabled()
} }
} }
} }

View File

@ -96,13 +96,14 @@ struct LogFileView: View {
private func startLogFileWatching() { private func startLogFileWatching() {
showingLogs = true showingLogs = true
self.readLatestLogFile()
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
if showingLogs { if showingLogs {
self.readLatestLogFile() self.readLatestLogFile()
} }
if isfps { if isfps {
sleep(1)
if get_current_fps() != 0 { if get_current_fps() != 0 {
stopLogFileWatching() stopLogFileWatching()
timer.invalidate() timer.invalidate()

View File

@ -48,11 +48,15 @@ struct SettingsView: View {
@AppStorage("showlogsgame") var showlogsgame: Bool = false @AppStorage("showlogsgame") var showlogsgame: Bool = false
@AppStorage("stick-button") var stickButton = false
@AppStorage("waitForVPN") var waitForVPN = false
@State private var showResolutionInfo = false @State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false @State private var showAnisotropicInfo = false
@State private var showControllerInfo = false @State private var showControllerInfo = false
@State private var searchText = "" @State private var searchText = ""
@AppStorage("portal") var gamepo = false @AppStorage("portal") var gamepo = false
@StateObject var ryujinx = Ryujinx.shared
var filteredMemoryModes: [(String, String)] { var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes } guard !searchText.isEmpty else { return memoryManagerModes }
@ -183,7 +187,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: {
@ -286,6 +290,11 @@ struct SettingsView: View {
}.tint(.blue) }.tint(.blue)
Toggle(isOn: $stickButton) {
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
}.tint(.blue)
Toggle(isOn: $ryuDemo) { Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
} }
@ -452,8 +461,21 @@ struct SettingsView: View {
.tint(.blue) .tint(.blue)
.contextMenu { .contextMenu {
Button { Button {
if let mainWindow = UIApplication.shared.windows.last { waitForVPN.toggle()
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) } label: {
Label {
Text("Wait for VPN")
} icon: {
if waitForVPN {
Image(systemName: "checkmark")
}
}
}
Button {
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, most kind, helpful and nice 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
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!) UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
@ -481,7 +503,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)
@ -537,7 +560,11 @@ struct SettingsView: View {
model.hasPrefix("iPhone") ? "iphone" : model.hasPrefix("iPhone") ? "iphone" :
"macwindow" "macwindow"
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill") labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
.onAppear() {
print("JIY ;(((((")
ryujinx.ryuIsJITEnabled()
}
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip") labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
@ -546,7 +573,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")
@ -564,10 +591,6 @@ struct SettingsView: View {
Section { Section {
DisclosureGroup { DisclosureGroup {
Toggle(isOn: $mVKPreFillBuffer) {
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
}.tint(.blue)
Toggle(isOn: $config.dfsIntegrityChecks) { Toggle(isOn: $config.dfsIntegrityChecks) {
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield") labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
}.tint(.blue) }.tint(.blue)
@ -577,7 +600,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 +649,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" : "")")
} }
} }
@ -626,6 +658,8 @@ struct SettingsView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.onAppear { .onAppear {
mVKPreFillBuffer = false
if let configs = loadSettings() { if let configs = loadSettings() {
self.config = configs self.config = configs
} else { } else {

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

@ -1,168 +0,0 @@
//
// GameDLCManagerSheet.swift
// MeloNX
//
// Created by XITRIX on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
var enabled: Bool
enum CodingKeys: String, CodingKey {
case fullPath = "path"
case titleId = "title_id"
case enabled = "is_enabled"
}
}
struct DownloadableContentContainer: Codable, Hashable {
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
enum CodingKeys: String, CodingKey {
case containerPath = "path"
case downloadableContentNcaList = "dlc_nca_list"
}
}
struct DLCManagerSheet: View {
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
var body: some View {
NavigationView {
let withIndex = dlcs.enumerated().map { $0 }
List(withIndex, id: \.element.containerPath) { index, dlc in
Button(action: {
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
Self.saveDlcs(game, dlc: dlcs)
}) {
HStack {
Text((dlc.containerPath as NSString).lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
try? FileManager.default.removeItem(atPath: path.path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} label: {
Text("Remove DLC")
}
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameDLC = true
}
}
}
.onAppear {
dlcs = Self.loadDlc(game)
}
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls):
for url in urls {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
if !fileManager.fileExists(atPath: dlcDirectory.path) {
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
guard !dlcContent.isEmpty else { return }
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
let container = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(container)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error copying game file: \(error)")
}
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
}
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
guard let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
guard let data = try? JSONEncoder().encode(dlc) else { return }
try? data.write(to: dlcJsonPath(for: game))
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
}
}
extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentDirectory
}
}

View File

@ -1,201 +0,0 @@
//
// GameUpdateManagerSheet.swift
// MeloNX
//
// Created by Stossy11 on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct UpdateManagerSheet: View {
@State private var items: [String] = []
@State private var paths: [URL] = []
@State private var selectedItem: String? = nil
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
var body: some View {
NavigationView {
List(paths, id: \..self, selection: $selectedItem) { item in
Button(action: {
selectItem(item.lastPathComponent)
}) {
HStack {
Text(item.lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
removeUpdate(item)
} label: {
Text("Remove Update")
}
}
}
.onAppear {
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
}
.navigationTitle("\(game!.titleName) Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameUpdate = true
}
}
}
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
let gameInfo = game!
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
if !fileManager.fileExists(atPath: updatedDirectory.path) {
try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
selectItem(url.lastPathComponent)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
loadJSON(jsonURL!)
} catch {
print("Error copying game file: \(error)")
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
func removeUpdate(_ game: URL) {
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
paths.removeAll { $0 == game }
items.removeAll { $0 == gameString }
if selectedItem == gameString {
selectedItem = nil
}
do {
try FileManager.default.removeItem(at: game)
} catch {
print(error)
}
saveJSON(selectedItem: selectedItem ?? "")
Ryujinx.shared.games = Ryujinx.shared.loadGames()
}
func saveJSON(selectedItem: String?) {
guard let jsonURL = jsonURL else { return }
do {
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
} catch {
print("Failed to update JSON: \(error)")
}
}
func loadJSON(_ json: URL) {
self.jsonURL = json
guard let jsonURL else { return }
do {
let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let list = jsonDict["paths"] as? [String]
{
let filteredList = list.filter { relativePath in
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
}
let urls: [URL] = filteredList.map { relativePath in
URL.documentsDirectory.appendingPathComponent(relativePath)
}
items = filteredList
paths = urls
selectedItem = jsonDict["selected"] as? String
}
} catch {
print("Failed to read JSON: \(error)")
createDefaultJSON()
}
}
func createDefaultJSON() {
guard let jsonURL = jsonURL else { return }
let defaultData: [String: Any] = ["selected": "", "paths": []]
do {
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL)
items = []
selectedItem = ""
} catch {
print("Failed to create default JSON: \(error)")
}
}
func selectItem(_ item: String) {
let newSelection = "updates/\(game!.titleId)/\(item)"
guard let jsonURL else { return }
do {
let data = try Data(contentsOf: jsonURL)
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
jsonDict["selected"] = ""
selectedItem = ""
} else {
jsonDict["selected"] = "\(newSelection)"
selectedItem = newSelection
}
jsonDict["paths"] = items
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to update JSON: \(error)")
}
}
}

View File

@ -0,0 +1,308 @@
//
// GameDLCManagerSheet.swift
// MeloNX
//
// Created by XITRIX on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
// MARK: - Models
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
var enabled: Bool
enum CodingKeys: String, CodingKey {
case fullPath = "path"
case titleId = "title_id"
case enabled = "is_enabled"
}
}
struct DownloadableContentContainer: Codable, Hashable, Identifiable {
var id: String { containerPath }
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
var filename: String {
(containerPath as NSString).lastPathComponent
}
var isEnabled: Bool {
downloadableContentNcaList.first?.enabled == true
}
enum CodingKeys: String, CodingKey {
case containerPath = "path"
case downloadableContentNcaList = "dlc_nca_list"
}
}
// MARK: - View
struct DLCManagerSheet: View {
// MARK: - Properties
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.dismiss) private var dismiss
// MARK: - Body
var body: some View {
iOSNav {
List {
if dlcs.isEmpty {
emptyStateView
} else {
ForEach(dlcs) { dlc in
dlcRow(dlc)
}
.onDelete(perform: removeDLCs)
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameDLC = true
} label: {
Label("Add DLC", systemImage: "plus")
}
}
}
.onAppear {
loadData()
}
}
.fileImporter(
isPresented: $isSelectingGameDLC,
allowedContentTypes: [.item],
allowsMultipleSelection: true,
onCompletion: handleFileImport
)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No DLCs Found",
systemImage: "puzzlepiece.extension",
description: Text("Tap the + button to add game DLCs.")
)
} else {
VStack(spacing: 20) {
Spacer()
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No DLCs Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game DLCs.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
Button {
toggleDLC(dlc)
} label: {
HStack {
Text(dlc.filename)
.foregroundStyle(.primary)
Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundStyle(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
removeDLC(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
// MARK: - Functions
private func loadData() {
dlcs = Self.loadDlc(game)
}
private func toggleDLC(_ dlc: DownloadableContentContainer) {
guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
let toggle = !dlcs[index].isEnabled
dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
var mutableNca = nca
mutableNca.enabled = toggle
return mutableNca
}
Self.saveDlcs(game, dlc: dlcs)
}
private func removeDLCs(at offsets: IndexSet) {
offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
}
private func removeDLC(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let dlcToRemove = dlcs[index]
let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
do {
try FileManager.default.removeItem(at: path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Failed to remove DLC: \(error)")
}
}
private func handleFileImport(result: Result<[URL], Error>) {
switch result {
case .success(let urls):
for url in urls {
importDLC(from: url)
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func importDLC(from url: URL) {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
// Copy the DLC file
let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.removeItem(at: destinationURL)
try fileManager.copyItem(at: url, to: destinationURL)
// Fetch DLC metadata from Ryujinx
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
guard !dlcContent.isEmpty else {
print("No valid DLC content found")
return
}
let newDlcContainer = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(newDlcContainer)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error importing DLC: \(error)")
}
}
}
// MARK: - Helper Methods
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard FileManager.default.fileExists(atPath: jsonURL.path),
let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
} catch {
print("Error loading DLCs: \(error)")
return []
}
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
do {
let data = try JSONEncoder().encode(dlc)
try data.write(to: dlcJsonPath(for: game))
} catch {
print("Error saving DLCs: \(error)")
}
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("dlc.json")
}
}
// MARK: - Array Extension
extension Array where Element: AnyObject {
mutating func mutableForEach(_ body: (inout Element) -> Void) {
for index in indices {
var element = self[index]
body(&element)
self[index] = element
}
}
}
// MARK: - URL Extension
extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentDirectory
}
}

View File

@ -0,0 +1,299 @@
//
// GameUpdateManagerSheet.swift
// MeloNX
//
// Created by Stossy11 on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct UpdateManagerSheet: View {
// MARK: - Properties
@State private var updates: [UpdateItem] = []
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
@Environment(\.dismiss) private var dismiss
// MARK: - Models
class UpdateItem: Identifiable, ObservableObject {
let id = UUID()
let url: URL
let filename: String
let path: String
@Published var isSelected: Bool = false
init(url: URL, filename: String, path: String, isSelected: Bool = false) {
self.url = url
self.filename = filename
self.path = path
self.isSelected = isSelected
}
}
// MARK: - Body
var body: some View {
iOSNav {
List {
if updates.isEmpty {
emptyStateView
} else {
ForEach(updates) { update in
updateRow(update)
}
.onDelete(perform: removeUpdates)
}
}
.navigationTitle("\(game?.titleName ?? "Game") Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameUpdate = true
} label: {
Label("Add Update", systemImage: "plus")
}
}
}
.onAppear {
loadData()
}
}
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No Updates Found",
systemImage: "arrow.down.circle",
description: Text("Tap the + button to add game updates.")
)
} else {
VStack(spacing: 20) {
Spacer()
Image(systemName: "arrow.down.circle")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No Updates Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game updates.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
private func updateRow(_ update: UpdateItem) -> some View {
Button {
toggleSelection(update)
} label: {
HStack {
Text(update.filename)
.foregroundStyle(.primary)
Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(update.isSelected ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = updates.firstIndex(where: { $0.path == update.path }) {
removeUpdate(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
// MARK: - Functions
private func loadData() {
guard let game = game else { return }
let documentsDirectory = URL.documentsDirectory
jsonURL = documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("updates.json")
loadJSON()
}
private func loadJSON() {
guard let jsonURL = jsonURL else { return }
do {
if !FileManager.default.fileExists(atPath: jsonURL.path) {
createDefaultJSON()
return
}
let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let paths = jsonDict["paths"] as? [String],
let selected = jsonDict["selected"] as? String {
let filteredPaths = paths.filter { relativePath in
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
}
updates = filteredPaths.map { relativePath in
let url = URL.documentsDirectory.appendingPathComponent(relativePath)
return UpdateItem(
url: url,
filename: url.lastPathComponent,
path: relativePath,
isSelected: selected == relativePath
)
}
}
} catch {
print("Failed to read JSON: \(error)")
createDefaultJSON()
}
}
private func createDefaultJSON() {
guard let jsonURL = jsonURL else { return }
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
let defaultData: [String: Any] = ["selected": "", "paths": []]
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL)
updates = []
} catch {
print("Failed to create default JSON: \(error)")
}
}
private func handleFileImport(result: Result<URL, Error>) {
switch result {
case .success(let selectedURL):
guard let game = game,
selectedURL.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { selectedURL.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
// Create directories if needed
try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
// Copy the file
let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
try? fileManager.removeItem(at: destinationURL) // Remove if exists
try fileManager.copyItem(at: selectedURL, to: destinationURL)
// Add to updates
let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
let newUpdate = UpdateItem(
url: destinationURL,
filename: selectedURL.lastPathComponent,
path: relativePath
)
updates.append(newUpdate)
toggleSelection(newUpdate)
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Error copying update file: \(error)")
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func toggleSelection(_ update: UpdateItem) {
print("toggle selection \(update.path)")
updates = updates.map { item in
var mutableItem = item
mutableItem.isSelected = item.path == update.path && !update.isSelected
print(mutableItem.isSelected)
print(update.isSelected)
return mutableItem
}
print(updates)
saveJSON()
}
private func removeUpdates(at offsets: IndexSet) {
offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
}
private func removeUpdate(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let updateToRemove = updates[index]
do {
// Remove the file
try FileManager.default.removeItem(at: updateToRemove.url)
// Remove from updates array
updates.remove(at: index)
// Save changes
saveJSON()
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to remove update: \(error)")
}
}
private func saveJSON() {
guard let jsonURL = jsonURL else { return }
do {
let paths = updates.map { $0.path }
let selected = updates.first(where: { $0.isSelected })?.path ?? ""
let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
} catch {
print("Failed to update JSON: \(error)")
}
}
}

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

@ -21,7 +21,7 @@ struct SetupView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { ZStack {
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.systemName.contains("iPadOS") {
iPadSetupView( iPadSetupView(
finished: $finished, finished: $finished,
isImportingKeys: $isImportingKeys, isImportingKeys: $isImportingKeys,
@ -65,7 +65,9 @@ struct SetupView: View {
initialize() initialize()
finished = false finished = false
keysImported = Ryujinx.shared.checkIfKeysImported() keysImported = Ryujinx.shared.checkIfKeysImported()
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
} }
} }
@ -116,6 +118,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,8 +370,8 @@ struct SetupView: View {
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path) Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") firmImported = (firmware == "" ? "0" : firmware) != "0"
alertMessage = "Firmware installed successfully" alertMessage = "Firmware installed successfully"
showAlert = true showAlert = true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,6 @@
framework module RyujinxHelper {
umbrella header "RyujinxHelper.h"
export *
module * { export * }
}

View File

@ -4,22 +4,22 @@
<dict> <dict>
<key>files</key> <key>files</key>
<dict> <dict>
<key>Headers/RyujinxKeyboard.h</key> <key>Headers/RyujinxHelper.h</key>
<data> <data>
5P7GN4g050n199pV6/+SpfMBgJc= 5P7GN4g050n199pV6/+SpfMBgJc=
</data> </data>
<key>Info.plist</key> <key>Info.plist</key>
<data> <data>
hYdI/ktAKwjBSfaJpt6Yc8UKLCY= UOH9NuuEcz5NQiQlrM2LNFaG2pI=
</data> </data>
<key>Modules/module.modulemap</key> <key>Modules/module.modulemap</key>
<data> <data>
0kFAMoTn+4Q1J/dM6uMLe3EhbL0= JDij7psMD6pZZpigUfkSQldib+I=
</data> </data>
</dict> </dict>
<key>files2</key> <key>files2</key>
<dict> <dict>
<key>Headers/RyujinxKeyboard.h</key> <key>Headers/RyujinxHelper.h</key>
<dict> <dict>
<key>hash2</key> <key>hash2</key>
<data> <data>
@ -30,7 +30,7 @@
<dict> <dict>
<key>hash2</key> <key>hash2</key>
<data> <data>
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ= 5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
</data> </data>
</dict> </dict>
</dict> </dict>

View File

@ -1,6 +0,0 @@
framework module RyujinxKeyboard {
umbrella header "RyujinxKeyboard.h"
export *
module * { export * }
}

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,114 @@ 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;
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++)
{
if (offset >= data.Length)
break;
int currentLayer = layer + i;
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
if (currentLayerSize <= 0)
break;
try
{
var layerData = data.Slice(offset, currentLayerSize);
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
offset += layerSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else if (region.HasValue)
{
var rect = region.Value;
if (rect.Width <= 0 || rect.Height <= 0)
return;
int dataPerPixel = data.Length / (rect.Width * rect.Height);
if (dataPerPixel <= 0)
return;
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);
if (chunkHeight <= 0)
break;
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
int chunkSize = chunkHeight * rowStride;
if (offset >= data.Length)
break;
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
if (safeChunkSize <= 0)
break;
try
{
var chunkData = data.Slice(offset, safeChunkSize);
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
currentY += chunkHeight;
offset += chunkSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else
{
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
}
}
private void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
{
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
if (chunkBufferLength <= 0)
return;
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 +884,52 @@ namespace Ryujinx.Graphics.Vulkan
_gd.PipelineInternal.EndRenderPass(); _gd.PipelineInternal.EndRenderPass();
} }
CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data); try
{
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)
{
var region = chunkRegion.Value;
if (region.Width <= 0 || region.Height <= 0)
return;
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
chunkBufferLength,
false,
chunkLayer,
chunkLevel,
region.X,
region.Y,
region.Width,
region.Height);
}
else
{ {
CopyFromOrToBuffer( CopyFromOrToBuffer(
cbs.CommandBuffer, cbs.CommandBuffer,
buffer, buffer,
image, image,
bufferDataLength, chunkBufferLength,
false, false,
layer, chunkLayer,
level, chunkLevel,
region.Value.X, chunkLayers,
region.Value.Y, chunkLevels,
region.Value.Width, chunkSingleSlice);
region.Value.Height);
} }
else }
catch (Exception e)
{ {
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice);
}
} }
} }

View File

@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets
StringLengthMax = _keyboardForegroundConfig.StringLengthMax, StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
InitialText = initialText, InitialText = initialText,
}; };
_device.UIHandler.DisplayInputDialog(args, inputText => _device.UIHandler.DisplayInputDialog(args, inputText =>
{ {
Console.WriteLine($"User entered: {inputText}"); Console.WriteLine($"User entered: {inputText}");

View File

@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.ResponseData.Write((int)_gyroscopeZeroDriftMode); context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode }); // Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
return ResultCode.Success; return ResultCode.Success;
} }

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

View File

@ -251,16 +251,17 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")] [UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS() public static unsafe int GetFPS()
{ {
if (_window != null) { if (_window == null || _window.Device == null)
{
return 0;
}
Switch Device = _window.Device; Switch Device = _window.Device;
int intValue = (int)Device.Statistics.GetGameFrameRate(); int intValue = (int)Device.Statistics.GetGameFrameRate();
return intValue; return intValue;
} }
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "initialize")] [UnmanagedCallersOnly(EntryPoint = "initialize")]
public static unsafe void Initialize() public static unsafe void Initialize()

View File

@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2
public bool DisplayMessageDialog(string title, string message) public bool DisplayMessageDialog(string title, string message)
{ {
if (OperatingSystem.IsIOS())
{
AlertHelper.ShowAlert(title, message, false);
} else {
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle); SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
}
return true; return true;
} }

View File

@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
{ {
public static class AlertHelper public static class AlertHelper
{ {
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showKeyboardAlert(string title, string message, string placeholder); public static extern void showKeyboardAlert(string title, string message, string placeholder);
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showAlert(string title, string message, bool showCancel);
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr getKeyboardInput(); private static extern IntPtr getKeyboardInput();
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern void clearKeyboardInput(); private static extern void clearKeyboardInput();
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered) public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2
} }
}); });
} }
public static void ShowAlert(string title, string message, bool cancel) {
showAlert(title, message, cancel);
}
} }
} }