Compare commits

...

26 Commits

Author SHA1 Message Date
Stossy11
0bb5389370 Add Sensitivity, Add Device Model, Memory Limit and more to logs, Disable JitStreamer EB in favour of StikJIT, Change Cache Size, Update Model Name in settings 2025-04-02 18:59:35 +11:00
Stossy11
8b81cb39d7 Fully Fix File Importer 2025-03-29 17:37:42 +11:00
Stossy11
ccdb8b76a8 Hopefully fix File Picker 2025-03-28 07:58:52 +11:00
37020a5026 Update LICENSE.txt 2025-03-24 08:49:09 +00:00
259f6c6872 Update LICENSE.txt 2025-03-24 08:39:21 +00:00
Stossy11
2b7e29fa21 Implement new Virtual Controller Joystick. Add Game Requirements 2025-03-24 17:26:25 +11:00
8917ebf708 revert d326f5a00b651fa331e37faa68fc9019b415f31e
im dumb

revert Fix typos

i should git blame it 😭
2025-03-23 02:25:23 +00:00
d326f5a00b Fix typos
i should git blame it 😭
2025-03-23 02:22:18 +00:00
3721a77cc4 Merge pull request 'Update to newer app icon (fits better into the bounding box)' (#23) from CycloKid/MeloNX:XC-ios-ht into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#23

melonx
2025-03-22 01:08:51 +00:00
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
56 changed files with 2789 additions and 1370 deletions

View File

@ -1,3 +1,12 @@
Currently licensed under the GNU AFFERO GENERAL PUBLIC LICENSE version 3, or any later version, at your choice.
You may obtain a copy of the license at <https://gnu.org/>
Copyright (c) Rhajune Park and contributors, 2025
For copyright infringement claims, please contact abuse@pythonplayer123.dev for expedited processing
Previously licensed under the MeloNX License.
MeloNX License MeloNX License
Copyright (c) MeloNX Team and Contributors Copyright (c) MeloNX Team and Contributors

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,8 +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 natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required. Motion controls are natively supported in most cases.
In all scenarios, you can set up everything inside the input configuration menu.
- **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 */
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 */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
@ -32,6 +31,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 */;
@ -46,13 +52,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 */;
@ -116,7 +115,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,
); );
@ -177,7 +176,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,
@ -203,7 +202,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
@ -294,14 +292,13 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */, 4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */, 4E80A98F2CD6F54500029585 /* MeloNX */,
); );
name = MeloNX; name = MeloNX;
packageProductDependencies = ( packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */,
); );
productName = MeloNX; productName = MeloNX;
@ -362,7 +359,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;
@ -393,7 +390,6 @@
mainGroup = 4E80A9842CD6F54500029585; mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
); );
preferredProjectObjectVersion = 56; preferredProjectObjectVersion = 56;
@ -453,7 +449,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 */
@ -482,6 +478,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 */;
@ -492,11 +494,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 */;
@ -708,8 +705,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/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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = fast; 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;
@ -827,10 +846,55 @@
"$(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",
"$(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",
"$(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 = com.stossy11.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;
@ -905,8 +969,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/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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = fast; 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;
@ -1024,10 +1110,55 @@
"$(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",
"$(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",
"$(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 = com.stossy11.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;
@ -1223,14 +1354,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = { 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchoe/SwiftSVG"; repositoryURL = "https://github.com/mchoe/SwiftSVG";
@ -1242,11 +1365,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
productName = SwiftUIJoystick;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = { 4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b", "originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805",
"pins" : [ "pins" : [
{ {
"identity" : "swiftsvg", "identity" : "swiftsvg",
@ -9,15 +9,6 @@
"branch" : "master", "branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
} }
},
{
"identity" : "swiftuijoystick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
"state" : {
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
"version" : "1.5.0"
}
} }
], ],
"version" : 3 "version" : 3

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1610" LastUpgradeVersion = "1620"
version = "1.7"> version = "2.0">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES"
@ -62,10 +62,13 @@
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
showGraphicsOverview = "Yes" allowLocationSimulation = "YES"
allowLocationSimulation = "YES"> queueDebuggingEnabled = "No"
consoleMode = "0"
structuredConsoleMode = "2">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
@ -105,8 +108,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

@ -0,0 +1,36 @@
//
// EnableJIT.swift
// MeloNX
//
// Created by Stossy11 on 10/02/2025.
//
import Foundation
import Network
import UIKit
func enableJITStik() {
let bundleid = Bundle.main.bundleIdentifier ?? "Unknown"
let address = URL(string: "stikjit://enable-jit?bundle-id=\(bundleid)")!
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
}
DispatchQueue.main.async {
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
// JIT, wow
} else {
fatalError("Unable to get Window")
}
}
}
task.resume()
}

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 {
@ -133,10 +131,8 @@ class VirtualController {
} }
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) { func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
let scaleFactor = 32767.0 / 160 let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
if stick == .right { if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue)) updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
@ -191,6 +187,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,13 +31,14 @@ struct iOSNav<Content: View>: View {
} }
class Ryujinx { class Ryujinx : ObservableObject {
private var isRunning = false private var isRunning = false
let virtualController = VirtualController() let virtualController = VirtualController()
@Published var controllerMap: [Controller] = [] @Published var controllerMap: [Controller] = []
@Published var metalLayer: CAMetalLayer? = nil @Published var metalLayer: CAMetalLayer? = nil
@Published var isPortrait = false
@Published var firmwareversion = "0" @Published var firmwareversion = "0"
@Published var emulationUIView: MeloMTKView? = nil @Published var emulationUIView: MeloMTKView? = nil
@Published var config: Ryujinx.Configuration? = nil @Published var config: Ryujinx.Configuration? = nil
@ -45,6 +46,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 +150,7 @@ class Ryujinx {
self.config = config self.config = config
RunLoop.current.perform { [self] in thread = Thread { [self] in
isRunning = true isRunning = true
@ -178,6 +183,10 @@ class Ryujinx {
Self.log("Emulation failed to start: \(error)") Self.log("Emulation failed to start: \(error)")
} }
} }
thread.qualityOfService = .userInteractive
thread.name = "MeloNX"
thread.start()
} }
@ -192,6 +201,7 @@ class Ryujinx {
self.metalLayer = nil self.metalLayer = nil
stop_emulation() stop_emulation()
thread.cancel()
} }
var running: Bool { var running: Bool {
@ -264,6 +274,24 @@ class Ryujinx {
// We don't need this. Ryujinx should handle it fine :3 // We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3 // this also causes crashes in some games :3
var model = ""
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
model = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
args.append(contentsOf: ["--device-model", model])
args.append(contentsOf: ["--device-display-name", UIDevice.modelName])
if checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") {
args.append("--has-memory-entitlement")
}
args.append(contentsOf: ["--system-language", config.language.rawValue]) args.append(contentsOf: ["--system-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue]) args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
@ -360,7 +388,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 +397,6 @@ class Ryujinx {
return firmwareVersion return firmwareVersion
} }
} catch {
print(error)
}
return "0" return "0"
} }
@ -501,66 +524,145 @@ 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)")
} }
public func updateOrientation() -> Bool {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
return (window.bounds.size.height > window.bounds.size.width)
}
return false
}
func ryuIsJITEnabled() {
jitenabled = isJITEnabled()
}
} }
public extension UIDevice {
static let modelName: String = {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
#if os(iOS)
switch identifier {
case "iPod5,1": return "iPod touch (5th generation)"
case "iPod7,1": return "iPod touch (6th generation)"
case "iPod9,1": return "iPod touch (7th generation)"
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
case "iPhone4,1": return "iPhone 4s"
case "iPhone5,1", "iPhone5,2": return "iPhone 5"
case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
case "iPhone7,2": return "iPhone 6"
case "iPhone7,1": return "iPhone 6 Plus"
case "iPhone8,1": return "iPhone 6s"
case "iPhone8,2": return "iPhone 6s Plus"
case "iPhone9,1", "iPhone9,3": return "iPhone 7"
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
case "iPhone10,1", "iPhone10,4": return "iPhone 8"
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
case "iPhone10,3", "iPhone10,6": return "iPhone X"
case "iPhone11,2": return "iPhone XS"
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
case "iPhone11,8": return "iPhone XR"
case "iPhone12,1": return "iPhone 11"
case "iPhone12,3": return "iPhone 11 Pro"
case "iPhone12,5": return "iPhone 11 Pro Max"
case "iPhone13,1": return "iPhone 12 mini"
case "iPhone13,2": return "iPhone 12"
case "iPhone13,3": return "iPhone 12 Pro"
case "iPhone13,4": return "iPhone 12 Pro Max"
case "iPhone14,4": return "iPhone 13 mini"
case "iPhone14,5": return "iPhone 13"
case "iPhone14,2": return "iPhone 13 Pro"
case "iPhone14,3": return "iPhone 13 Pro Max"
case "iPhone14,7": return "iPhone 14"
case "iPhone14,8": return "iPhone 14 Plus"
case "iPhone15,2": return "iPhone 14 Pro"
case "iPhone15,3": return "iPhone 14 Pro Max"
case "iPhone15,4": return "iPhone 15"
case "iPhone15,5": return "iPhone 15 Plus"
case "iPhone16,1": return "iPhone 15 Pro"
case "iPhone16,2": return "iPhone 15 Pro Max"
case "iPhone17,3": return "iPhone 16"
case "iPhone17,4": return "iPhone 16 Plus"
case "iPhone17,1": return "iPhone 16 Pro"
case "iPhone17,2": return "iPhone 16 Pro Max"
case "iPhone17,5": return "iPhone 16e"
case "iPhone8,4": return "iPhone SE"
case "iPhone12,8": return "iPhone SE (2nd generation)"
case "iPhone14,6": return "iPhone SE (3rd generation)"
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
case "iPad5,3", "iPad5,4": return "iPad Air 2"
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
case "iPad5,1", "iPad5,2": return "iPad mini 4"
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
case "AppleTV5,3": return "Apple TV"
case "AppleTV6,2": return "Apple TV 4K"
case "AudioAccessory1,1": return "HomePod"
case "AudioAccessory5,1": return "HomePod mini"
case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
default: return identifier
}
#elseif os(tvOS)
switch identifier {
case "AppleTV5,3": return "Apple TV 4"
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
default: return identifier
}
#elseif os(visionOS)
switch identifier {
case "RealityDevice14,1": return "Apple Vision Pro"
default: return identifier
}
#endif
}
return mapToDevice(identifier: identifier)
}()
}

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

@ -0,0 +1,38 @@
//
// LatestVersionResponse.swift
// MeloNX
//
// Created by Bella on 12/03/2025.
//
struct LatestVersionResponse: Codable {
let version_number: String
let version_number_stripped: String
let changelog: String
let download_link: String
#if DEBUG
static let example1 = LatestVersionResponse(
version_number: "1.0.0",
version_number_stripped: "100",
changelog: """
- Rewrite Display Code (SDL isn't used for display anymore)
- Add New Onboarding / Setup
- Better Performance
- Remove "SDL Window" option in settings
- Fix JIT Cache Regions
- Fix how JIT is detected in Settings
- Fix ABYX being swapped on controller.
- Settings are now a config.json file
- Fix Performance Overlay not showing when Virtual Controller is hidden
- Add displaying logs when Loading or in-game
- Fix Launching games from outside of the roms folder
- Add Waiting for JIT popup
- Fix spesific Games
- Added Back Herobrine ("You were supposed to be the hero, Bryan")
""",
download_link: "https://example.com"
)
#endif
}

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?
@ -37,6 +37,7 @@ struct ContentView: View {
// JIT // JIT
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("stikJIT") var stikJIT: Bool = false
// Other Configuration // Other Configuration
@State var isMK8: Bool = false @State var isMK8: Bool = false
@ -53,145 +54,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 +220,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 +257,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 +273,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 +318,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 +330,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 +340,70 @@ 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 jitStreamerEB {
jitStreamerEB = false // byee jitstreamer eb
}
if !ryujinx.jitenabled {
if useTrollStore {
askForJIT()
} else if stikJIT {
enableJITStik()
} 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

@ -7,105 +7,172 @@
import SwiftUI import SwiftUI
import GameController import GameController
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
@State var hideDpad = false
@State var hideABXY = false
@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 {
JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
}
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView()
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
}
}
}
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: 20) {
ShoulderButtonsViewLeft()
ZStack {
JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 20) {
ShoulderButtonsViewRight()
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView()
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
}
}
}
}
}
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 +185,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 +208,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: 7) {
ButtonView(button: .dPadUp) ButtonView(button: .dPadUp)
HStack { HStack(spacing: 22) {
ButtonView(button: .dPadLeft) ButtonView(button: .dPadLeft)
Spacer(minLength: 20) Spacer(minLength: 22)
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 +233,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: 7) {
ButtonView(button: .X) ButtonView(button: .X)
HStack { HStack(spacing: 22) {
ButtonView(button: .Y) ButtonView(button: .Y)
Spacer(minLength: 20) Spacer(minLength: 22)
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 +259,102 @@ 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.5) : Color.black.opacity(0.5))
.opacity(isPressed ? 0.4 : 0.7) .background(
Group {
if !button.isTrigger && button != .leftStick && button != .rightStick {
Circle()
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
.frame(width: width * 1.25, height: height * 1.25)
} else if button == .leftStick || button == .rightStick {
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))
} else if button.isTrigger {
Image(systemName: "" + String(turntobutton(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 { print(String(buttonText.dropFirst(2)))
width = 65 configureSizeForButton()
}
} }
private func turntobutton(_ string: String) -> String {
var sting = string
if string.hasPrefix("zl") || string.hasPrefix("zr") {
sting = String(string.dropFirst(3))
} else {
sting = String(string.dropFirst(2))
}
sting = sting.replacingOccurrences(of: "rectangle", with: "button")
sting = sting.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
if button == .back || button == .start || button == .guide { return sting
}
private func handleButtonPress() {
if !isPressed {
isPressed = true
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 +363,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 +374,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:
@ -267,7 +387,7 @@ struct ButtonView: View {
case .dPadRight: case .dPadRight:
return "arrowtriangle.right.circle.fill" return "arrowtriangle.right.circle.fill"
case .leftTrigger: case .leftTrigger:
return"zl.rectangle.roundedtop.fill" return "zl.rectangle.roundedtop.fill"
case .rightTrigger: case .rightTrigger:
return "zr.rectangle.roundedtop.fill" return "zr.rectangle.roundedtop.fill"
case .leftShoulder: case .leftShoulder:
@ -275,16 +395,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

@ -0,0 +1,88 @@
//
// Joystick.swift
// MeloNX
//
// Created by Stossy11 on 21/03/2025.
//
import SwiftUI
struct Joystick: View {
@Binding var position: CGPoint
@State var joystickSize: CGFloat
var boundarySize: CGFloat
@State private var offset: CGSize = .zero
@Binding var showBackground: Bool
let sensitivity: CGFloat = 1.5
var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
withAnimation(.easeIn) {
showBackground = true
}
let translation = value.translation
let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
let maxRadius = (boundarySize - joystickSize) / 2
let extendedRadius = maxRadius + (joystickSize / 2)
if distance <= extendedRadius {
offset = translation
} else {
let angle = atan2(translation.height, translation.width)
offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius)
}
position = CGPoint(
x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
)
}
.onEnded { _ in
offset = .zero
position = .zero
withAnimation(.easeOut) {
showBackground = false
}
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.clear.opacity(0))
.frame(width: boundarySize, height: boundarySize)
if showBackground {
Circle()
.fill(Color.gray.opacity(0.4))
.frame(width: boundarySize, height: boundarySize)
.animation(.easeInOut(duration: 0.05), value: showBackground)
.transition(.scale)
}
Circle()
.fill(Color.white.opacity(0.5))
.frame(width: joystickSize, height: joystickSize)
.background(
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: joystickSize * 1.25, height: joystickSize * 1.25)
)
.offset(offset)
.gesture(dragGesture)
}
.frame(width: boundarySize, height: boundarySize)
.onChange(of: showBackground) { newValue in
if newValue {
joystickSize *= 1.4
} else {
joystickSize = (boundarySize * 0.2)
}
}
}
}

View File

@ -7,13 +7,13 @@
// //
import SwiftUI import SwiftUI
import SwiftUIJoystick
public struct Joystick: View { struct JoystickController: View {
@State var iscool: Bool? = nil @State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@ObservedObject public var joystickMonitor = JoystickMonitor() @Binding var showBackground: Bool
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State var position: CGPoint = CGPoint(x: 0, y: 0)
var dragDiameter: CGFloat { var dragDiameter: CGFloat {
var selfs = CGFloat(160) var selfs = CGFloat(160)
selfs *= controllerScale selfs *= controllerScale
@ -23,24 +23,11 @@ public struct Joystick: View {
return selfs return selfs
} }
private let shape: JoystickShape = .circle
public var body: some View { public var body: some View {
VStack{ VStack {
JoystickBuilder( Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
monitor: self.joystickMonitor, .onChange(of: position) { newValue in
width: self.dragDiameter,
shape: .circle,
background: {
Text("")
.hidden()
},
foreground: {
Circle().fill(Color.gray)
.opacity(0.7)
},
locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
let scaledX = Float(newValue.x) let scaledX = Float(newValue.x)
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/ let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
print("Joystick Position: (\(scaledX), \(scaledY))") print("Joystick Position: (\(scaledX), \(scaledY))")

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

@ -0,0 +1,44 @@
//
// GameRequirementsCache.swift
// MeloNX
//
// Created by Stossy11 on 21/03/2025.
//
import Foundation
class GameCompatibiliryCache {
static let shared = GameCompatibiliryCache()
private let cacheKey = "gameRequirementsCache"
private let timestampKey = "gameRequirementsCacheTimestamp"
private let cacheDuration: TimeInterval = Double.random(in: 3...5) * 24 * 60 * 60 // Randomly pick 3-5 days
func getCachedData() -> [GameRequirements]? {
guard let cachedData = UserDefaults.standard.data(forKey: cacheKey),
let timestamp = UserDefaults.standard.object(forKey: timestampKey) as? Date else {
return nil
}
let timeElapsed = Date().timeIntervalSince(timestamp)
if timeElapsed > cacheDuration {
clearCache()
return nil
}
return try? JSONDecoder().decode([GameRequirements].self, from: cachedData)
}
func setCachedData(_ data: [GameRequirements]) {
if let encodedData = try? JSONEncoder().encode(data) {
UserDefaults.standard.set(encodedData, forKey: cacheKey)
UserDefaults.standard.set(Date(), forKey: timestampKey)
}
}
func clearCache() {
UserDefaults.standard.removeObject(forKey: cacheKey)
UserDefaults.standard.removeObject(forKey: timestampKey)
}
}

View File

@ -29,7 +29,9 @@ 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?
@State var gameRequirements: [GameRequirements] = []
var games: Binding<[Game]> { var games: Binding<[Game]> {
Binding( Binding(
get: { Ryujinx.shared.games }, get: { Ryujinx.shared.games },
@ -78,7 +80,7 @@ struct GameLibraryView: View {
if !isSearching && !realRecentGames.isEmpty { if !isSearching && !realRecentGames.isEmpty {
Section { Section {
ForEach(realRecentGames) { game in ForEach(realRecentGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) { Button(role: .destructive) {
removeFromRecentGames(game) removeFromRecentGames(game)
@ -93,14 +95,14 @@ struct GameLibraryView: View {
Section { Section {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
} }
} header: { } header: {
Text("Others") Text("Others")
} }
} else { } else {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
} }
} }
} }
@ -112,6 +114,15 @@ struct GameLibraryView: View {
let firmware = Ryujinx.shared.fetchFirmwareVersion() let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware) firmwareversion = (firmware == "" ? "0" : firmware)
pullGameCompatibility() { game in
switch game {
case .success(let sucees):
gameRequirements = sucees
case .failure(_):
print("uhohh stinki")
}
}
} }
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result { switch result {
@ -203,6 +214,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 +276,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)
@ -359,7 +377,6 @@ extension Game: Codable {
version = try container.decode(String.self, forKey: .version) version = try container.decode(String.self, forKey: .version)
fileURL = try container.decode(URL.self, forKey: .fileURL) fileURL = try container.decode(URL.self, forKey: .fileURL)
// Initialize other properties
self.containerFolder = fileURL.deletingLastPathComponent() self.containerFolder = fileURL.deletingLastPathComponent()
self.fileType = .item self.fileType = .item
} }
@ -378,10 +395,11 @@ extension Game: Codable {
struct GameListRow: View { struct GameListRow: View {
let game: Game let game: Game
@Binding var startemu: Game? @Binding var startemu: Game?
@Binding var games: [Game] // Add this binding @Binding var games: [Game]
@Binding var isViewingGameInfo: Bool @Binding var isViewingGameInfo: Bool
@Binding var isSelectingGameUpdate: Bool @Binding var isSelectingGameUpdate: Bool
@Binding var isSelectingGameDLC: Bool @Binding var isSelectingGameDLC: Bool
@Binding var gameRequirements: [GameRequirements]
@Binding var gameInfo: Game? @Binding var gameInfo: Game?
@State var gametoDelete: Game? @State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false @State var showGameDeleteConfirmation: Bool = false
@ -425,8 +443,31 @@ struct GameListRow: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Spacer() Spacer()
if let game = gameRequirements.first(where: { $0.game_id == game.titleId }) {
let totalMemory = ProcessInfo.processInfo.physicalMemory
VStack(spacing: 10) {
Capsule().fill(game.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.green : Color.red)
.frame(width: 70 / 1.5, height: 35 / 1.5)
.overlay {
Text(game.device_memory)
.foregroundStyle(.white)
.font(.system(size: 11))
}
Capsule().fill(game.color)
.frame(width: 70 / 1.5, height: 35 / 1.5)
.overlay {
Text(game.compatibility)
.foregroundStyle(.white)
.font(.system(size: 10))
}
}
}
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
.font(.title2) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
@ -492,6 +533,7 @@ struct GameListRow: View {
} }
} }
private func deleteGame(game: Game) { private func deleteGame(game: Game) {
let fileManager = FileManager.default let fileManager = FileManager.default
do { do {
@ -502,3 +544,66 @@ struct GameListRow: View {
} }
} }
} }
struct GameRequirements: Codable {
var game_id: String
var compatibility: String
var device_memory: String
var memoryInt: Int {
var devicemem = device_memory
devicemem.removeLast(2)
print(devicemem)
return Int(devicemem) ?? 0
}
var color: Color {
switch compatibility {
case "Perfect":
return .green
case "Playable":
return .yellow
case "Menu":
return .orange
case "Boots":
return .red
case "Nothing":
return .black
default:
return .clear
}
}
}
func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Error>) -> Void) {
if let cachedData = GameCompatibiliryCache.shared.getCachedData() {
completion(.success(cachedData))
return
}
guard let url = URL(string: "https://melonx.org/api/game_entries") else {
completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
return
}
do {
let decodedData = try JSONDecoder().decode([GameRequirements].self, from: data)
GameCompatibiliryCache.shared.setCachedData(decodedData)
completion(.success(decodedData))
} catch {
completion(.failure(error))
}
}
task.resume()
}

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

@ -19,6 +19,7 @@ struct SettingsView: View {
@AppStorage("useTrollStore") var useTrollStore: Bool = false @AppStorage("useTrollStore") var useTrollStore: Bool = false
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("stikJIT") var stikJIT: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
@ -48,11 +49,17 @@ 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
@AppStorage("HideButtons") var hideButtonsJoy = 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 +190,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 +293,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")
} }
@ -446,14 +458,29 @@ struct SettingsView: View {
.tint(.blue) .tint(.blue)
if #available(iOS 17.0.1, *) { if #available(iOS 17.0.1, *) {
// You will stay in our hearts, JitStreamer EB. one of the first public JIT enablers, that didn't need a computer after initial install
/*
Toggle(isOn: $jitStreamerEB) { Toggle(isOn: $jitStreamerEB) {
labelWithIcon("JitStreamer EB", iconName: "bolt.heart") labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
} }
.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")!)
@ -469,6 +496,32 @@ struct SettingsView: View {
Text("About") Text("About")
} }
} }
*/
Toggle(isOn: $stikJIT) {
labelWithIcon("StikJIT", iconName: "bolt.heart")
}
.tint(.blue)
.contextMenu {
Button {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About StikJIT", message: "StikJIT is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert)
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
UIApplication.shared.open(URL(string: "https://github.com/0-Blu/StikJIT")!)
}
alertController.addAction(learnMoreButton)
let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
alertController.addAction(doneButton)
mainWindow.rootViewController?.present(alertController, animated: true)
}
} label: {
Text("About")
}
}
} else { } else {
Toggle(isOn: $useTrollStore) { Toggle(isOn: $useTrollStore) {
labelWithIcon("TrollStore JIT", iconName: "troll.svg") labelWithIcon("TrollStore JIT", iconName: "troll.svg")
@ -481,7 +534,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)
@ -529,27 +583,29 @@ struct SettingsView: View {
Section { Section {
let totalMemory = ProcessInfo.processInfo.physicalMemory let totalMemory = ProcessInfo.processInfo.physicalMemory
let model = getDeviceModel() let model = getDeviceModel()
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
model.hasPrefix("iPhone") ? "iOS" :
"macOS"
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" : let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
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")
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName) labelWithIcon("Device: \(UIDevice.modelName)", iconName: iconName)
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("\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", iconName: "applelogo")
} header: { } header: {
Text("Information") Text("Information")
@ -564,10 +620,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 +629,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 +678,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 +687,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

@ -0,0 +1,60 @@
//
// MeloNXUpdateSheet.swift
// MeloNX
//
// Created by Stossy11 and Bella on 12/03/2025.
//
import SwiftUI
struct MeloNXUpdateSheet: View {
let updateInfo: LatestVersionResponse
@Binding var isPresented: Bool
var body: some View {
iOSNav {
VStack {
Text("Version \(updateInfo.version_number) is available. You are currently on Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown").")
VStack {
Text("Changelog:")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.headline)
ScrollView {
Text(updateInfo.changelog)
.padding()
}
.frame(maxHeight: 400)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.padding(.top, 15)
Spacer()
Button(action: {
if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url)
}
}) {
Text("Download Now")
.font(.title3)
.bold()
.frame(width: 300, height: 40)
}
.buttonStyle(.borderedProminent)
.frame(alignment: .bottom)
}
.padding(.horizontal)
.navigationTitle("Version \(updateInfo.version_number) Available!")
.toolbar {
Button(action: {
isPresented = false
}) {
Text("Close")
}
}
}
}
}

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,8 +8,13 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
import CryptoKit import CryptoKit
import UniformTypeIdentifiers
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
@main @main
struct MeloNXApp: App { struct MeloNXApp: App {
@ -18,16 +23,39 @@ struct MeloNXApp: App {
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State var alert: UIAlertController? = nil @State var alert: UIAlertController? = nil
@State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil
@State var finished = false @State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
init() {
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
method_exchangeImplementations(origMethod, fixMethod)
}
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ZStack {
if showed || DRM != 1 {
if finishedStorage { if finishedStorage {
ContentView() ContentView()
.onAppear {
checkLatestVersion()
}
.sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
if !newValue {
showOutOfDateSheet = false
updateInfo = nil
}
}
)) {
if let updateInfo = updateInfo {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
} else { } else {
SetupView(finished: $finished) SetupView(finished: $finished)
.onChange(of: finished) { newValue in .onChange(of: finished) { newValue in
@ -38,241 +66,50 @@ struct MeloNXApp: App {
} }
} }
} }
} else {
Group {
VStack {
Spacer()
HStack {
Text("Loading...")
ProgressView()
}
Spacer()
Text(UIDevice.current.identifierForVendor?.uuidString ?? "")
}
}
.onAppear {
initR()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(1))
.foregroundColor(.white)
}
}
} }
} }
func initR() { func checkLatestVersion() {
if DRM == 1 { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
DispatchQueue.main.async { [self] in let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
// drmcheck()
InitializeRyujinx() { bool in #if DEBUG
if bool { let urlString = "http://192.168.178.116:8000/api/latest_release"
print("Ryujinx Files Initialized Successfully") #else
DispatchQueue.main.async { [self] in let urlString = "https://melonx.org/api/latest_release"
withAnimation { #endif
showed = true
guard let url = URL(string: urlString) else {
print("Invalid URL")
return
} }
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in let task = URLSession.shared.dataTask(with: url) { data, response, error in
InitializeRyujinx() { bool in
if !bool, (scenePhase != .background || scenePhase == .inactive) {
withAnimation {
showed = false
}
if !(alert?.isViewLoaded ?? false) {
alert = showDMCAAlert()
}
} else {
DispatchQueue.main.async {
alert?.dismiss(animated: true)
showed = true
}
}
}
}
}
} else {
showDMCAAlert()
}
}
}
}
}
func showAlert() -> UIAlertController? {
// Create the alert controller
if let mainWindow = UIApplication.shared.windows.last {
let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
// Add a text field to the alert
alertController.addTextField { textField in
textField.placeholder = "Enter key here"
}
// Add the "OK" action
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
// Get the text entered in the text field
if let textField = alertController.textFields?.first, let enteredText = textField.text {
print("Entered text: \(enteredText)")
UserDefaults.standard.set(enteredText, forKey: "MeloDRMID")
// drmcheck() { bool in
// if bool {
// showed = true
// } else {
// exit(0)
// }
// }
}
}
alertController.addAction(okAction)
// Add a "Cancel" action
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
// Present the alert
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
return alertController
} else {
return nil
}
}
}
func showDMCAAlert() -> UIAlertController? {
if let mainWindow = UIApplication.shared.windows.first {
let alertController = UIAlertController(title: "Unauthorized Copy Notice", message: "This app was illegally leaked. Please report the download on the MeloNX Discord. In the meantime, check out Pomelo! \n -Stossy11", preferredStyle: .alert)
DispatchQueue.main.async {
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
}
return alertController
} else {
// uhoh
return nil
}
}
/*
func drmcheck(completion: @escaping (Bool) -> Void) {
if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() {
if let value = UserDefaults.standard.string(forKey: "MeloDRMID") {
if let url = URL(string: "https://mx.stossy11.com/auth/\(value)/\(base64device)") {
print(url)
// Create a URLSession
let session = URLSession.shared
// Create a data task
let task = session.dataTask(with: url) { data, response, error in
// Handle errors
if let error = error { if let error = error {
exit(0) print("Error checking for new version: \(error)")
return
} }
// Check response and data guard let data = data else {
if let response = response as? HTTPURLResponse, response.statusCode == 200 { print("No data received")
print("Successfully Recieved API Data") return
completion(true) }
} else if let response = response as? HTTPURLResponse, response.statusCode == 201 {
print("Successfully Created Auth UUID") do {
completion(true) let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data)
} else { let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
completion(false)
if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 {
DispatchQueue.main.async {
updateInfo = latestVersionResponse
showOutOfDateSheet = true
}
}
} catch {
print("Failed to decode response: \(error)")
} }
} }
// Start the task
task.resume() task.resume()
} }
} else {
completion(false)
}
} else {
completion(false)
}
}
*/
func InitializeRyujinx(completion: @escaping (Bool) -> Void) {
let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v"
guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else {
completion(false)
return
}
if (detectRoms(path: path) != value) {
completion(false)
}
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.urlCache = nil
let session = URLSession(configuration: configuration)
guard let url = URL(string: addFolders(path)!) else {
completion(false)
return
}
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
completion(false)
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(false)
return
}
if httpResponse.statusCode == 200 {
completion(true)
} else {
completion(false)
}
return
}
task.resume()
}
func detectRoms(path string: String) -> String {
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,13 +4,14 @@ 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
{ {
class NoWxCache : IDisposable class NoWxCache : IDisposable
{ {
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024; private const int SharedCacheSize = 512 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024; private const int LocalCacheSize = 128 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there // How many calls to the same function we allow until we pad the shared cache to force the function to become available there
@ -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

@ -609,23 +609,64 @@ namespace Ryujinx.Graphics.Vulkan
public byte[] GetData(int x, int y, int width, int height) public byte[] GetData(int x, int y, int width, int height)
{ {
int size = width * height * Info.BytesPerPixel; const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
int size = width * height * Info.BytesPerPixel;
byte[] bitmap = new byte[size];
if (size <= MaxChunkSize)
{
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
using (var cbs = _gd.CommandBufferPool.Rent()) using (var cbs = _gd.CommandBufferPool.Rent())
{ {
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = GetImage().Get(cbs).Value; var image = GetImage().Get(cbs).Value;
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height); CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
} }
bufferHolder.WaitForFences(); bufferHolder.WaitForFences();
byte[] bitmap = new byte[size];
GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap); GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap);
return bitmap; return bitmap;
} }
int dataPerPixel = Info.BytesPerPixel;
int rowStride = width * dataPerPixel;
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
int originalHeight = height;
int currentY = y;
int bitmapOffset = 0;
while (currentY < y + originalHeight)
{
int chunkHeight = Math.Min(rowsPerChunk, y + originalHeight - currentY);
if (chunkHeight <= 0)
break;
int chunkSize = chunkHeight * rowStride;
// Process this chunk
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkSize);
using (var cbs = _gd.CommandBufferPool.Rent())
{
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = GetImage().Get(cbs).Value;
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, chunkSize, true, 0, 0, x, currentY, width, chunkHeight);
}
bufferHolder.WaitForFences();
GetDataFromBuffer(bufferHolder.GetDataStorage(0, chunkSize), chunkSize, Span<byte>.Empty)
.CopyTo(new Span<byte>(bitmap, bitmapOffset, chunkSize));
currentY += chunkHeight;
bitmapOffset += chunkSize;
}
return bitmap;
}
public PinnedSpan<byte> GetData() public PinnedSpan<byte> GetData()
{ {
BackgroundResource resources = _gd.BackgroundResources.Get(); BackgroundResource resources = _gd.BackgroundResources.Get();
@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
return GetDataFromBuffer(result, size, result); return GetDataFromBuffer(result, size, result);
} }
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level) private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer = 0, int level = 0)
{ {
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
int size = GetBufferDataLength(Info.GetMipSize(level)); int size = GetBufferDataLength(Info.GetMipSize(level));
if (size <= MaxChunkSize)
{
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level); Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
return GetDataFromBuffer(result, size, result); return GetDataFromBuffer(result, size, result);
} }
byte[] fullResult = new byte[size];
Span<byte> fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level);
GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult);
return fullResult;
}
/// <inheritdoc/> /// <inheritdoc/>
public void SetData(MemoryOwner<byte> data) public void SetData(MemoryOwner<byte> data)
{ {
@ -769,16 +824,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 * 96; // 96MB Chunks
int bufferDataLength = GetBufferDataLength(data.Length); int bufferDataLength = GetBufferDataLength(data.Length);
using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength); if (bufferDataLength <= MaxChunkSize)
{
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
return;
}
Auto<DisposableImage> imageAuto = GetImage(); if (!region.HasValue && !singleSlice && layers > 1)
{
int layerSize = data.Length / layers;
int offset = 0;
// Load texture data inline if the texture has been used on the current command buffer. for (int i = 0; i < layers; i++)
{
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 +939,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

@ -29,6 +29,17 @@ namespace Ryujinx.Headless.SDL2
[Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")] [Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
public int ExclusiveFullscreenHeight { get; set; } public int ExclusiveFullscreenHeight { get; set; }
// Host Information
[Option("device-model", Required = false, HelpText = "Set the current iDevice Model")]
public string DeviceModel { get; set; }
[Option("has-memory-entitlement", Required = false, HelpText = "If the increased memory entitlement exists.")]
public bool MemoryEnt { get; set; }
[Option("device-display-name", Required = false, HelpText = "Set the current iDevice display name.")]
public string DisplayName { get; set; }
// Input // Input
[Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")] [Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")]

View File

@ -251,21 +251,21 @@ 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()
{ {
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
if (_virtualFileSystem == null) if (_virtualFileSystem == null)
@ -1153,6 +1153,11 @@ namespace Ryujinx.Headless.SDL2
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
} }
if (OperatingSystem.IsIOS()) {
Logger.Info?.Print(LogClass.Application, $"Current Device: {option.DisplayName} ({option.DeviceModel}) {Environment.OSVersion.Version}");
Logger.Info?.Print(LogClass.Application, $"Increased Memory Limit: {option.MemoryEnt}");
}
GraphicsConfig.EnableShaderCache = true; GraphicsConfig.EnableShaderCache = true;
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())

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

View File

@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
{ {
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping; private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
private readonly List<string> _gamepadsIds; private readonly List<string> _gamepadsIds;
private readonly object _lock = new object();
public ReadOnlySpan<string> GamepadsIds => _gamepadsIds.ToArray(); public ReadOnlySpan<string> GamepadsIds
{
get
{
lock (_lock)
{
return _gamepadsIds.ToArray();
}
}
}
public string DriverName => "SDL2"; public string DriverName => "SDL2";
@ -35,7 +45,7 @@ namespace Ryujinx.Input.SDL2
} }
} }
private static string GenerateGamepadId(int joystickIndex) private string GenerateGamepadId(int joystickIndex)
{ {
Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex); Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
@ -44,14 +54,16 @@ namespace Ryujinx.Input.SDL2
return null; return null;
} }
// Include joystickIndex at the start of the ID to maintain compatibility with GetJoystickIndexByGamepadId
return joystickIndex + "-" + guid; return joystickIndex + "-" + guid;
} }
private static int GetJoystickIndexByGamepadId(string id) private int GetJoystickIndexByGamepadId(string id)
{ {
string[] data = id.Split("-"); string[] data = id.Split("-");
if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex)) // Parse the joystick index from the ID string
if (data.Length < 2 || !int.TryParse(data[0], out int joystickIndex))
{ {
return -1; return -1;
} }
@ -64,7 +76,11 @@ namespace Ryujinx.Input.SDL2
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id)) if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
{ {
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId); _gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
lock (_lock)
{
_gamepadsIds.Remove(id); _gamepadsIds.Remove(id);
}
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
} }
@ -74,6 +90,13 @@ namespace Ryujinx.Input.SDL2
{ {
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE) if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
{ {
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
{
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
// so it is rejected to avoid doubling the entries.
return;
}
string id = GenerateGamepadId(joystickDeviceId); string id = GenerateGamepadId(joystickDeviceId);
if (id == null) if (id == null)
@ -81,16 +104,21 @@ namespace Ryujinx.Input.SDL2
return; return;
} }
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before // Check if we already have this gamepad ID in our list
// so it is rejected to avoid doubling the entries. lock (_lock)
{
if (_gamepadsIds.Contains(id)) if (_gamepadsIds.Contains(id))
{ {
return; return;
} }
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id)) if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
{
lock (_lock)
{ {
_gamepadsIds.Add(id); _gamepadsIds.Add(id);
}
OnGamepadConnected?.Invoke(id); OnGamepadConnected?.Invoke(id);
} }
@ -104,12 +132,16 @@ namespace Ryujinx.Input.SDL2
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected; SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected; SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
// Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds) foreach (string id in _gamepadsIds)
{ {
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
} }
lock (_lock)
{
_gamepadsIds.Clear(); _gamepadsIds.Clear();
}
SDL2Driver.Instance.Dispose(); SDL2Driver.Instance.Dispose();
} }
@ -130,11 +162,6 @@ namespace Ryujinx.Input.SDL2
return null; return null;
} }
if (id != GenerateGamepadId(joystickIndex))
{
return null;
}
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex); IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == IntPtr.Zero) if (gamepadHandle == IntPtr.Zero)